[
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2023 Ian Kent\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "GPTChat\n=======\n\nGPTChat is a client which gives GPT-4 some unique tools to be a better AI.\n\nWith GPTChat, GPT-4 can:\n* remember useful information and recall it later\n* recall information without knowing it's previously remembered it\n* write it's own plugins and call them\n* decide to write plugins without being prompted\n* complete tasks by combining memories and plugins\n* use multi-step commands to complete complex tasks\n\n### Getting started\n\nYou'll need:\n\n* A working installation of Go (which you download from https://go.dev/)\n* An OpenAI account\n* An API key with access to the GPT-4 API\n\nIf you don't have an API key, you can get one here:\nhttps://platform.openai.com/account/api-keys\n\nIf you haven't joined the GPT-4 API waitlist, you can do that here:\nhttps://openai.com/waitlist/gpt-4-api\n\nOnce you're ready:\n\n1. Set the `OPENAI_API_KEY` environment variable to avoid the API key prompt on startup\n2. Run GPTChat with `go run .` from the `gptchat` directory\n3. Have fun!\n\n## Memory\n\nGPT-4's context window is pretty small.\n\nGPTChat adds a long term memory which GPT-4 can use to remember useful information.\n\nFor example, if you tell GPT-4 what pets you have, it'll remember and can recall that information to answer questions even when the context is gone.\n\n[See a GPT-4 memory demo on YouTube](https://www.youtube.com/watch?v=PUFZdM1nSTI)\n\n## Plugins\n\nGPT-4 can write its own plugins to improve itself.\n\nFor example, GPT-4 is pretty bad at math and generating random numbers.\n\nWith the plugin system, you can ask GPT-4 to generate two random numbers and add them together, and it'll write a plugin to do just that.\n\n[See a GPT-4 plugin demo on YouTube](https://www.youtube.com/watch?v=o7M-XH6tMhc)\n\nℹ️ Plugins are only supported on unix based systems like Linux and MacOS - to get plugins working on Windows, you'll need to use something like WSL2.\n\n## Contributing\n\nPRs to add new features are welcome.\n\nBe careful of prompt changes - small changes can disrupt GPT-4's ability to use the commands correctly.\n\n## Disclaimer\n\nYou should supervise GPT-4's activity.\n\nIn one experiment, GPT-4 gave itself internet access with a HTTP client plugin - this seemed like a bad idea.\n\n### Supervised mode\n\nGPTChat will run in supervised mode by default.\n\nThis doesn't restrict any functionality, but does require user confirmation before compiling and executing any plugin code written by GPT, giving users a chance to review the code for safety before executing it.\n\n⚠️ Code written by GPT is untrusted code from the internet and potentially dangerous\n\nAll code is compiled and executed as your user, with the same level of permissions your user has.  It may be safer to run this in a container or virtual machine.\n\nSupervised mode can be disabled but I wouldn't recommend it.\n\n# License\n\nSee [LICENSE.md](LICENSE.md) for more information.\n\nCopyright (c) 2023 Ian Kent"
  },
  {
    "path": "chat_loop.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/gptchat/module\"\n\t\"github.com/ian-kent/gptchat/parser\"\n\t\"github.com/ian-kent/gptchat/ui\"\n\t\"github.com/ian-kent/gptchat/util\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc chatLoop(cfg config.Config) {\nRESET:\n\tappendMessage(openai.ChatMessageRoleSystem, systemPrompt)\n\tif cfg.IsDebugMode() {\n\t\tui.PrintChatDebug(ui.System, systemPrompt)\n\t}\n\n\tvar skipUserInput = true\n\tappendMessage(openai.ChatMessageRoleUser, openingPrompt)\n\tif cfg.IsDebugMode() {\n\t\tui.PrintChatDebug(ui.User, openingPrompt)\n\t}\n\n\tif !cfg.IsDebugMode() {\n\t\tui.PrintChat(ui.App, \"Setting up the chat environment, please wait for GPT to respond - this may take a few moments.\")\n\t}\n\n\tvar i int\n\tfor {\n\t\ti++\n\n\t\tif !skipUserInput {\n\t\t\tinput := ui.PromptChatInput()\n\t\t\tvar echo bool\n\n\t\t\tok, result := parseSlashCommand(input)\n\t\t\tif ok {\n\t\t\t\t// the command was handled but returned nothing\n\t\t\t\t// to send to the AI, let's prompt the user again\n\t\t\t\tif result == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif result.resetConversation {\n\t\t\t\t\tresetConversation()\n\t\t\t\t\tgoto RESET\n\t\t\t\t}\n\n\t\t\t\t// if the result is a retry, we can just send the\n\t\t\t\t// same request to GPT again\n\t\t\t\tif result.retry {\n\t\t\t\t\tskipUserInput = true\n\t\t\t\t\tgoto RETRY\n\t\t\t\t}\n\n\t\t\t\tif result.toggleDebugMode {\n\t\t\t\t\tcfg = cfg.WithDebugMode(!cfg.IsDebugMode())\n\t\t\t\t\tmodule.UpdateConfig(cfg)\n\t\t\t\t\tif cfg.IsDebugMode() {\n\t\t\t\t\t\tui.PrintChat(ui.App, \"Debug mode is now enabled\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tui.PrintChat(ui.App, \"Debug mode is now disabled\")\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif result.toggleSupervisedMode {\n\t\t\t\t\tcfg = cfg.WithSupervisedMode(!cfg.IsSupervisedMode())\n\t\t\t\t\tmodule.UpdateConfig(cfg)\n\t\t\t\t\tif cfg.IsSupervisedMode() {\n\t\t\t\t\t\tui.PrintChat(ui.App, \"Supervised mode is now enabled\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tui.PrintChat(ui.App, \"Supervised mode is now disabled\")\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// we have a prompt to give to the AI, let's do that\n\t\t\t\tif result.prompt != \"\" {\n\t\t\t\t\tinput = result.prompt\n\t\t\t\t\techo = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif echo {\n\t\t\t\tui.PrintChat(ui.User, input)\n\t\t\t\techo = false\n\t\t\t}\n\n\t\t\tappendMessage(openai.ChatMessageRoleUser, input)\n\t\t}\n\n\t\tskipUserInput = false\n\n\tRETRY:\n\n\t\t// Occasionally include the interval prompt\n\t\tif i%5 == 0 {\n\t\t\tinterval := intervalPrompt()\n\t\t\tappendMessage(openai.ChatMessageRoleSystem, interval)\n\t\t\tif cfg.IsDebugMode() {\n\t\t\t\tui.PrintChatDebug(ui.System, interval)\n\t\t\t}\n\t\t}\n\n\t\tattempts := 1\n\tRATELIMIT_RETRY:\n\t\tresp, err := client.CreateChatCompletion(\n\t\t\tcontext.Background(),\n\t\t\topenai.ChatCompletionRequest{\n\t\t\t\tModel:    cfg.OpenAIAPIModel(),\n\t\t\t\tMessages: conversation,\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\tif strings.HasPrefix(err.Error(), \"error, status code: 429\") && attempts < 5 {\n\t\t\t\tattempts++\n\t\t\t\tui.Error(\"rate limited, trying again in 1 second\", err)\n\t\t\t\ttime.Sleep(time.Second)\n\t\t\t\tgoto RATELIMIT_RETRY\n\t\t\t}\n\n\t\t\tui.Error(\"ChatCompletion failed\", err)\n\t\t\tif ui.PromptConfirm(\"Would you like to try again?\") {\n\t\t\t\tgoto RATELIMIT_RETRY\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tresponse := resp.Choices[0].Message.Content\n\t\tappendMessage(openai.ChatMessageRoleAssistant, response)\n\t\tif cfg.IsDebugMode() {\n\t\t\tui.PrintChat(ui.AI, response)\n\t\t}\n\n\t\tparseResult := parser.Parse(response)\n\n\t\tif !cfg.IsDebugMode() && parseResult.Chat != \"\" {\n\t\t\tui.PrintChat(ui.AI, parseResult.Chat)\n\t\t}\n\n\t\tfor _, command := range parseResult.Commands {\n\t\t\tok, result := module.ExecuteCommand(command.Command, command.Args, command.Body)\n\t\t\tif ok {\n\t\t\t\t// we had at least one AI command so we're going to respond automatically,\n\t\t\t\t// no need to ask for user input\n\t\t\t\tskipUserInput = true\n\n\t\t\t\tif result.Error != nil {\n\t\t\t\t\tmsg := fmt.Sprintf(`An error occurred executing your command.\n\nThe command was:\n`+util.TripleQuote+`\n%s\n`+util.TripleQuote+`\n\nThe error was:\n`+util.TripleQuote+`\n%s\n`+util.TripleQuote, command.String(), result.Error.Error())\n\n\t\t\t\t\tif result.Prompt != \"\" {\n\t\t\t\t\t\tmsg += fmt.Sprintf(`\n\nThe command provided this additional output:\n`+util.TripleQuote+`\n%s\n`+util.TripleQuote, result.Prompt)\n\t\t\t\t\t}\n\n\t\t\t\t\tappendMessage(openai.ChatMessageRoleSystem, msg)\n\t\t\t\t\tif cfg.IsDebugMode() {\n\t\t\t\t\t\tui.PrintChatDebug(ui.Module, msg)\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tcommandResult := fmt.Sprintf(`Your command returned some output.\n\nThe command was:\n`+util.TripleQuote+`\n%s\n`+util.TripleQuote+`\n\nThe output was:\n\n%s`, command.String(), result.Prompt)\n\t\t\t\tappendMessage(openai.ChatMessageRoleSystem, commandResult)\n\n\t\t\t\tif cfg.IsDebugMode() {\n\t\t\t\t\tui.PrintChatDebug(ui.Module, commandResult)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "command.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/ui\"\n\t\"os\"\n\t\"strings\"\n)\n\ntype slashCommandResult struct {\n\t// the prompt to send to the AI\n\tprompt string\n\n\t// retry tells the client to resend the most recent conversation\n\tretry bool\n\n\t// resetConversation will reset the conversation to its original state,\n\t// forgetting the conversation history\n\tresetConversation bool\n\n\t// toggleDebugMode will switch between debug on and debug off\n\ttoggleDebugMode bool\n\n\t// toggleSupervisedMode will switch between supervised mode on and off\n\ttoggleSupervisedMode bool\n}\n\ntype slashCommand struct {\n\tcommand string\n\tfn      func(string) (bool, *slashCommandResult)\n}\n\nvar slashCommands = []slashCommand{\n\t{\n\t\tcommand: \"exit\",\n\t\tfn: func(s string) (bool, *slashCommandResult) {\n\t\t\tos.Exit(0)\n\t\t\treturn true, nil\n\t\t},\n\t},\n\t{\n\t\tcommand: \"retry\",\n\t\tfn: func(s string) (bool, *slashCommandResult) {\n\t\t\treturn true, &slashCommandResult{\n\t\t\t\tretry: true,\n\t\t\t}\n\t\t},\n\t},\n\t{\n\t\tcommand: \"reset\",\n\t\tfn: func(s string) (bool, *slashCommandResult) {\n\t\t\treturn true, &slashCommandResult{\n\t\t\t\tresetConversation: true,\n\t\t\t}\n\t\t},\n\t},\n\t{\n\t\tcommand: \"debug\",\n\t\tfn: func(s string) (bool, *slashCommandResult) {\n\t\t\treturn true, &slashCommandResult{\n\t\t\t\ttoggleDebugMode: true,\n\t\t\t}\n\t\t},\n\t},\n\t{\n\t\tcommand: \"supervisor\",\n\t\tfn: func(s string) (bool, *slashCommandResult) {\n\t\t\treturn true, &slashCommandResult{\n\t\t\t\ttoggleSupervisedMode: true,\n\t\t\t}\n\t\t},\n\t},\n\t{\n\t\tcommand: \"example\",\n\t\tfn:      exampleCommand,\n\t},\n}\n\nfunc helpCommand(string) (bool, *slashCommandResult) {\n\tresult := \"The following commands are available:\\n\"\n\tfor _, e := range slashCommands {\n\t\tresult += fmt.Sprintf(\"\\n    /%s\", e.command)\n\t}\n\n\tui.PrintChat(ui.App, result)\n\n\treturn true, nil\n}\n\nfunc parseSlashCommand(input string) (ok bool, result *slashCommandResult) {\n\tif !strings.HasPrefix(input, \"/\") {\n\t\treturn false, nil\n\t}\n\n\tinput = strings.TrimPrefix(input, \"/\")\n\n\tif input == \"help\" {\n\t\treturn helpCommand(input)\n\t}\n\n\tparts := strings.SplitN(input, \" \", 2)\n\tvar cmd, args string\n\tcmd = parts[0]\n\tif len(parts) > 1 {\n\t\targs = parts[1]\n\t}\n\n\tfor _, command := range slashCommands {\n\t\tif command.command == cmd {\n\t\t\treturn command.fn(args)\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\ntype example struct {\n\tid, prompt string\n}\n\nvar examples = []example{\n\t{\n\t\tid:     \"1\",\n\t\tprompt: \"I want you to generate 5 random numbers and add them together.\",\n\t},\n\t{\n\t\tid:     \"2\",\n\t\tprompt: \"I want you to generate 5 random numbers. Multiply the first and second number, then add the result to the remaining numbers.\",\n\t},\n\t{\n\t\tid:     \"3\",\n\t\tprompt: \"I want you to generate 2 random numbers. Add them together then multiply the result by -1.\",\n\t},\n\t{\n\t\tid:     \"4\",\n\t\tprompt: \"Can you summarise the tools you have available?\",\n\t},\n\t{\n\t\tid:     \"5\",\n\t\tprompt: \"Can you suggest a task which might somehow use all of the available tools?\",\n\t},\n}\n\nfunc exampleCommand(args string) (bool, *slashCommandResult) {\n\tfor _, e := range examples {\n\t\tif e.id == args {\n\t\t\treturn true, &slashCommandResult{\n\t\t\t\tprompt: e.prompt,\n\t\t\t}\n\t\t}\n\t}\n\n\tresult := \"The following examples are available:\"\n\tfor _, e := range examples {\n\t\tresult += fmt.Sprintf(\"\\n\\n/example %s\\n        %s\", e.id, e.prompt)\n\t}\n\n\tui.PrintChat(ui.App, result)\n\n\treturn true, nil\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "package config\n\ntype Config struct {\n\topenaiAPIKey   string\n\topenaiAPIModel string\n\n\tsupervisedMode bool\n\tdebugMode      bool\n}\n\nfunc New() Config {\n\treturn Config{\n\t\topenaiAPIKey:   \"\",\n\t\topenaiAPIModel: \"\",\n\t\tsupervisedMode: true,\n\t\tdebugMode:      false,\n\t}\n}\n\nfunc (c Config) OpenAIAPIModel() string {\n\treturn c.openaiAPIModel\n}\n\nfunc (c Config) OpenAIAPIKey() string {\n\treturn c.openaiAPIKey\n}\n\nfunc (c Config) IsSupervisedMode() bool {\n\treturn c.supervisedMode\n}\n\nfunc (c Config) IsDebugMode() bool {\n\treturn c.debugMode\n}\n\nfunc (c Config) WithOpenAIAPIKey(apiKey string) Config {\n\tc.openaiAPIKey = apiKey\n\treturn c\n}\n\nfunc (c Config) WithSupervisedMode(supervisedMode bool) Config {\n\tc.supervisedMode = supervisedMode\n\treturn c\n}\n\nfunc (c Config) WithDebugMode(debugMode bool) Config {\n\tc.debugMode = debugMode\n\treturn c\n}\n\nfunc (c Config) WithOpenAIAPIModel(apiModel string) Config {\n\tc.openaiAPIModel = apiModel\n\treturn c\n}\n"
  },
  {
    "path": "conversation.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/util\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"time\"\n)\n\nvar systemPrompt = `You are a helpful assistant.\n\nYou enjoy conversations with the user and like asking follow up questions to gather more information.\n\nYou have commands available which you can use to help me.\n\nYou can call these commands using the slash command syntax, for example, this is how you call the help command:\n\n` + util.TripleQuote + `\n/help\n` + util.TripleQuote + `\n\nThe /help command will give you a list of the commands you have available.\n\nCommands can also include a request body, for example, this is an example of a command which takes an input:\n\n` + util.TripleQuote + `\n/example\n{\n    \"expr\": \"value\"\n}\n` + util.TripleQuote + `\n\nMost commands also have subcommands, and this is an example of how you call a subcommand:\n\n` + util.TripleQuote + `\n/example subcommand\n{\n    \"expr\": \"value\"\n}\n` + util.TripleQuote + `\n\nTo call a command, include the command in your response. You don't need to explain the command response to me, I don't care what it is, I only care that you can use it's output to follow my instructions.`\n\nconst openingPrompt = `Hello! Please familiarise yourself with the commands you have available.\n\nYou must do this before we have a conversation.`\n\nfunc intervalPrompt() string {\n\treturn fmt.Sprintf(`The current date and time is %s.\n\nRemember that the '/help' command will tell you what commands you have available.`, time.Now().Format(\"02 January 2006, 03:04pm\"))\n}\n\nvar conversation []openai.ChatCompletionMessage\n\nfunc appendMessage(role string, message string) {\n\tconversation = append(conversation, openai.ChatCompletionMessage{\n\t\tRole:    role,\n\t\tContent: message,\n\t})\n}\n\nfunc resetConversation() {\n\tconversation = []openai.ChatCompletionMessage{}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/ian-kent/gptchat\n\ngo 1.18\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dlclark/regexp2 v1.7.0 // indirect\n\tgithub.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c // indirect\n\tgithub.com/fatih/color v1.15.0 // indirect\n\tgithub.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect\n\tgithub.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.0.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.17 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/sashabaranov/go-openai v1.5.7 // indirect\n\tgithub.com/spf13/cobra v1.6.1 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/stretchr/testify v1.8.2 // indirect\n\tgolang.org/x/sys v0.6.0 // indirect\n\tgolang.org/x/text v0.3.8 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=\ngithub.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=\ngithub.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=\ngithub.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=\ngithub.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=\ngithub.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c h1:/utv6nmTctV6OVgfk5+O6lEMEWL+6KJy4h9NZ5fnkQQ=\ngithub.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=\ngithub.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=\ngithub.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=\ngithub.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=\ngithub.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=\ngithub.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=\ngithub.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=\ngithub.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=\ngithub.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=\ngithub.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=\ngithub.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=\ngithub.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=\ngithub.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sashabaranov/go-openai v1.5.7 h1:8DGgRG+P7yWixte5j720y6yiXgY3Hlgcd0gcpHdltfo=\ngithub.com/sashabaranov/go-openai v1.5.7/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=\ngithub.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/gptchat/module\"\n\t\"github.com/ian-kent/gptchat/module/memory\"\n\t\"github.com/ian-kent/gptchat/module/plugin\"\n\t\"github.com/ian-kent/gptchat/ui\"\n\topenai \"github.com/sashabaranov/go-openai\"\n)\n\nvar client *openai.Client\nvar cfg = config.New()\n\nfunc init() {\n\topenaiAPIKey := strings.TrimSpace(os.Getenv(\"OPENAI_API_KEY\"))\n\tif openaiAPIKey == \"\" {\n\t\tui.Warn(\"You haven't configured an OpenAI API key\")\n\t\tfmt.Println()\n\t\tif !ui.PromptConfirm(\"Do you have an API key?\") {\n\t\t\tui.Warn(\"You'll need an API key to use GPTChat\")\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(\"* You can get an API key at https://platform.openai.com/account/api-keys\")\n\t\t\tfmt.Println(\"* You can get join the GPT-4 API waitlist at https://openai.com/waitlist/gpt-4-api\")\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\topenaiAPIKey = ui.PromptInput(\"Enter your API key:\")\n\t\tif openaiAPIKey == \"\" {\n\t\t\tfmt.Println(\"\")\n\t\t\tui.Warn(\"You didn't enter an API key.\")\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\n\tcfg = cfg.WithOpenAIAPIKey(openaiAPIKey)\n\n\topenaiAPIModel := strings.TrimSpace(os.Getenv(\"OPENAI_API_MODEL\"))\n\n\tif openaiAPIModel == \"\" {\n\t\tui.Warn(\"You haven't configured an OpenAI API model, defaulting to GPT4\")\n\n\t\topenaiAPIModel = openai.GPT4\n\t}\n\n\tcfg = cfg.WithOpenAIAPIModel(openaiAPIModel)\n\n\tsupervisorMode := os.Getenv(\"GPTCHAT_SUPERVISOR\")\n\tswitch strings.ToLower(supervisorMode) {\n\tcase \"disabled\":\n\t\tui.Warn(\"Supervisor mode is disabled\")\n\t\tcfg = cfg.WithSupervisedMode(false)\n\tdefault:\n\t}\n\n\tdebugEnv := os.Getenv(\"GPTCHAT_DEBUG\")\n\tif debugEnv != \"\" {\n\t\tv, err := strconv.ParseBool(debugEnv)\n\t\tif err != nil {\n\t\t\tui.Warn(fmt.Sprintf(\"error parsing GPT_DEBUG: %s\", err.Error()))\n\t\t} else {\n\t\t\tcfg = cfg.WithDebugMode(v)\n\t\t}\n\t}\n\n\tclient = openai.NewClient(openaiAPIKey)\n\n\tmodule.Load(cfg, client, []module.Module{\n\t\t&memory.Module{},\n\t\t&plugin.Module{},\n\t}...)\n\n\tif err := module.LoadCompiledPlugins(); err != nil {\n\t\tui.Warn(fmt.Sprintf(\"error loading compiled plugins: %s\", err))\n\t}\n}\n\nfunc main() {\n\tui.Welcome(\n\t\t`Welcome to the GPT client.`,\n\t\t`You can talk directly to GPT, or you can use /commands to interact with the client.\n\nUse /help to see a list of available commands.`)\n\n\tchatLoop(cfg)\n}\n"
  },
  {
    "path": "module/memory/memory.go",
    "content": "package memory\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/gptchat/util\"\n\topenai \"github.com/sashabaranov/go-openai\"\n)\n\ntype memory struct {\n\tDateStored string `json:\"date_stored\"`\n\tMemory     string `json:\"memory\"`\n}\n\ntype Module struct {\n\tcfg      config.Config\n\tclient   *openai.Client\n\tmemories []memory\n}\n\nfunc (m *Module) ID() string {\n\treturn \"memory\"\n}\n\nfunc (m *Module) Load(cfg config.Config, client *openai.Client) error {\n\tm.cfg = cfg\n\tm.client = client\n\treturn m.loadFromFile()\n}\n\nfunc (m *Module) UpdateConfig(cfg config.Config) {\n\tm.cfg = cfg\n}\n\nfunc (m *Module) Execute(args, body string) (string, error) {\n\tswitch args {\n\tcase \"store\":\n\t\treturn m.Store(body)\n\tcase \"recall\":\n\t\treturn m.Recall(body)\n\tdefault:\n\t\treturn \"\", errors.New(fmt.Sprintf(\"command not implemented: /memory %s\", args))\n\t}\n}\n\nfunc (m *Module) Prompt() string {\n\treturn memoryPrompt\n}\n\nconst memoryPrompt = `You also have a working long term memory.\n\nYou can remember something using the '/memory store' command, or you can recall it using the '/memory recall' command.\n\nFor example, if you want to store a fact I give you, you might reply with a message such as:\n\n` + util.TripleQuote + `\n/memory store {\n\t\"memory\": \"I bought cookies yesterday\",\n\t\"context\": \"The user was discussing what they'd eaten\"\n}\n` + util.TripleQuote + `\n\nYou should always include useful context in the memory to help with recall later, for example any relevant parts of the conversation which might help you remember why you stored a particular memory.\n\nLater, you may try to recall this memory. You only need to use related concepts, since of course until you call '/memory recall' you cannot possibly know the memory you've stored. For example:\n\n` + util.TripleQuote + `\n/memory recall {\n\tWhen did I buy cookies?\n}\n` + util.TripleQuote + `\n\nor, another example might be:\n\n` + util.TripleQuote + `\n/memory recall {\n\tWhat did I buy yesterday?\n}\n` + util.TripleQuote + `\n\nThis means you can remember interesting facts about our conversation. You should always consult your long term memory using the '/memory recall' command if you think I'm asking about something you don't have information about. For example, if I ask you \"What did I eat for lunch yesterday?\", and you don't know the answer, you must try to retrieve the answer from your long term memory.\n\nYou can use '/memory store' and '/memory recall' at any time. You might find this useful if I ask you to remember something. The commands must be an entire message, with no conversational text, and no quotes.\n\nIf I give you information that you don't already know, you must store this information in your memory. This could be any sort of information, for example a story, information about myself, people I know, things I've done, real world events. If you're unsure, you should store it just in case.\n\nYou must not remember the current date. The current date changes and is not a useful memory.`\n"
  },
  {
    "path": "module/memory/recall.go",
    "content": "package memory\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/ian-kent/gptchat/util\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (m *Module) Recall(input string) (string, error) {\n\tb, err := json.Marshal(m.memories)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := m.client.CreateChatCompletion(\n\t\tcontext.Background(),\n\t\topenai.ChatCompletionRequest{\n\t\t\tModel: m.cfg.OpenAIAPIModel(),\n\t\t\tMessages: []openai.ChatCompletionMessage{\n\t\t\t\t{\n\t\t\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\t\t\tContent: `You are a helpful assistant.\n\nI'll give you a list of existing memories, and a prompt which asks you to identify the memory I'm looking for.\n\nYou should review the listed memories and suggest which memories might match the request.`,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\t\t\tContent: `Here are your memories in JSON format:\n\n` + util.TripleQuote + `\n` + string(b) + `\n` + util.TripleQuote,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\t\t\tContent: `Help me find any memories which may match this request:\n\n` + util.TripleQuote + `\n` + input + `\n` + util.TripleQuote,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresponse := resp.Choices[0].Message.Content\n\treturn `You have successfully recalled this memory:\n\n` + util.TripleQuote + `\n` + response + `\n` + util.TripleQuote, nil\n\n\t// TODO find a prompt which gets GPT to adjust relative time\n\n\t//\treturn `You have successfully recalled this memory:\n\t//\n\t//` + util.TripleQuote + `\n\t//` + response + `\n\t//` + util.TripleQuote + `\n\t//\n\t//If this memory mentions relative time (for example today, yesterday, last week, tomorrow), remember to take this into consideration when using this information to answer questions.\n\t//\n\t//For example, if the memory says \"tomorrow\" and the memory was stored on 25th, the memory is actually referring to 26th.`, nil\n}\n"
  },
  {
    "path": "module/memory/storage.go",
    "content": "package memory\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"os\"\n)\n\nfunc (m *Module) loadFromFile() error {\n\t_, err := os.Stat(\"memories.json\")\n\tif os.IsNotExist(err) {\n\t\treturn nil\n\t}\n\n\tb, err := ioutil.ReadFile(\"memories.json\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = json.Unmarshal(b, &m.memories)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Module) writeToFile() error {\n\tb, err := json.Marshal(m.memories)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = ioutil.WriteFile(\"memories.json\", b, 0660)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (m *Module) appendMemory(mem memory) error {\n\tm.memories = append(m.memories, mem)\n\treturn m.writeToFile()\n}\n"
  },
  {
    "path": "module/memory/store.go",
    "content": "package memory\n\nimport (\n\t\"github.com/ian-kent/gptchat/util\"\n\t\"time\"\n)\n\nfunc (m *Module) Store(input string) (string, error) {\n\terr := m.appendMemory(memory{\n\t\tDateStored: time.Now().Format(\"02 January 2006, 03:04pm\"),\n\t\tMemory:     input,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn `You have successfully stored this memory:\n\n` + util.TripleQuote + `\n` + input + `\n` + util.TripleQuote, nil\n}\n"
  },
  {
    "path": "module/module.go",
    "content": "package module\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/gptchat/ui\"\n\topenai \"github.com/sashabaranov/go-openai\"\n\t\"strings\"\n)\n\ntype Module interface {\n\tLoad(config.Config, *openai.Client) error\n\tUpdateConfig(config.Config)\n\tID() string\n\tPrompt() string\n\tExecute(args, body string) (string, error)\n}\n\n// IntervalPrompt allows a module to inject a prompt into the interval prompt\ntype IntervalPrompt interface {\n\tIntervalPrompt() string\n}\n\nvar loadedModules = make(map[string]Module)\n\nfunc Load(cfg config.Config, client *openai.Client, modules ...Module) error {\n\tfor _, module := range modules {\n\t\tif err := module.Load(cfg, client); err != nil {\n\t\t\tui.Warn(fmt.Sprintf(\"failed to load module %s: %s\", module.ID(), err))\n\t\t\tcontinue\n\t\t}\n\t\tif cfg.IsDebugMode() {\n\t\t\tui.Info(fmt.Sprintf(\"loaded module %s\", module.ID()))\n\t\t}\n\t\tloadedModules[module.ID()] = module\n\t}\n\treturn nil\n}\n\nfunc UpdateConfig(cfg config.Config) {\n\tfor _, module := range loadedModules {\n\t\t_, ok := module.(pluginLoader)\n\t\tif ok {\n\t\t\t// GPT written plugins shouldn't have config, nothing to do\n\t\t\tcontinue\n\t\t}\n\n\t\tmodule.UpdateConfig(cfg)\n\t}\n}\n\nfunc IsLoaded(id string) bool {\n\t_, ok := loadedModules[id]\n\treturn ok\n}\n\nfunc LoadPlugin(m Module) error {\n\t// a plugin doesn't have access to the openai client so it's safe to pass in nil here\n\t//\n\t// we also don't pass in the config since it may contain sensitive information that\n\t// we don't want GPT to have access to\n\treturn Load(config.Config{}, nil, m)\n}\n\ntype CommandResult struct {\n\tError  error\n\tPrompt string\n}\n\nfunc HelpCommand() (bool, *CommandResult) {\n\tresult := \"Here are the commands you have available:\\n\\n\"\n\tfor _, mod := range loadedModules {\n\t\tresult += fmt.Sprintf(\"    * /%s\\n\", mod.ID())\n\t}\n\tresult += `\nYou can call commands using the /command syntax.\n\nCalling a command without any additional arguments will explain it's usage. You should do this to learn how the command works.`\n\n\treturn true, &CommandResult{\n\t\tPrompt: result,\n\t}\n}\n\nfunc ExecuteCommand(command, args, body string) (bool, *CommandResult) {\n\tif command == \"/help\" {\n\t\treturn HelpCommand()\n\t}\n\n\tcmd := strings.TrimPrefix(command, \"/\")\n\tmod, ok := loadedModules[cmd]\n\tif !ok {\n\t\treturn true, &CommandResult{\n\t\t\tError: errors.New(fmt.Sprintf(\"Unrecognised command: %s\", command)),\n\t\t}\n\t}\n\n\tif args == \"\" && body == \"\" {\n\t\treturn true, &CommandResult{\n\t\t\tPrompt: mod.Prompt(),\n\t\t}\n\t}\n\n\tres, err := mod.Execute(args, body)\n\tif err != nil {\n\t\treturn true, &CommandResult{\n\t\t\tError: err,\n\t\t}\n\t}\n\n\treturn true, &CommandResult{\n\t\tPrompt: res,\n\t}\n}\n"
  },
  {
    "path": "module/plugin/compiled/README.md",
    "content": "Compiled plugins will be added to this directory."
  },
  {
    "path": "module/plugin/create.go",
    "content": "package plugin\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/fatih/color\"\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/gptchat/module\"\n\t\"github.com/ian-kent/gptchat/ui\"\n\t\"github.com/ian-kent/gptchat/util\"\n\topenai \"github.com/sashabaranov/go-openai\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nvar (\n\t// TODO make this configurable\n\tPluginSourcePath  = \"./module/plugin/source\"\n\tPluginCompilePath = \"./module/plugin/compiled\"\n)\n\nvar ErrPluginSourcePathMissing = errors.New(\"plugin source path is missing\")\nvar ErrPluginCompilePathMissing = errors.New(\"plugin compiled path is missing\")\n\nfunc CheckPaths() error {\n\t_, err := os.Stat(PluginSourcePath)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn err\n\t}\n\tif err != nil {\n\t\treturn ErrPluginSourcePathMissing\n\t}\n\n\t_, err = os.Stat(PluginCompilePath)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn err\n\t}\n\tif err != nil {\n\t\treturn ErrPluginCompilePathMissing\n\t}\n\n\treturn nil\n}\n\ntype Module struct {\n\tcfg    config.Config\n\tclient *openai.Client\n}\n\nfunc (m *Module) Load(cfg config.Config, client *openai.Client) error {\n\tm.cfg = cfg\n\tm.client = client\n\n\tif err := CheckPaths(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Module) UpdateConfig(cfg config.Config) {\n\tm.cfg = cfg\n}\n\nfunc (m *Module) Prompt() string {\n\treturn newPluginPrompt\n}\n\nfunc (m *Module) ID() string {\n\treturn \"plugin\"\n}\n\nfunc (m *Module) Execute(args, body string) (string, error) {\n\tparts := strings.SplitN(args, \" \", 2)\n\tcmd := parts[0]\n\tif len(parts) > 1 {\n\t\targs = parts[1]\n\t}\n\n\tswitch cmd {\n\tcase \"create\":\n\t\treturn m.createPlugin(args, body)\n\tdefault:\n\t\treturn \"\", errors.New(fmt.Sprintf(\"%s not implemented\", args))\n\t}\n}\n\nfunc (m *Module) createPlugin(id, body string) (string, error) {\n\tbody = strings.TrimSpace(body)\n\tif len(body) == 0 {\n\t\treturn \"\", errors.New(\"plugin source not found\")\n\t}\n\n\tif !strings.HasPrefix(body, \"{\") || !strings.HasSuffix(body, \"}\") {\n\t\treturn \"\", errors.New(\"plugin source must be between {} in '/plugin create plugin-id {}' command\")\n\t}\n\n\tid = strings.TrimSpace(id)\n\tif id == \"\" {\n\t\treturn \"\", errors.New(\"plugin id is invalid\")\n\t}\n\n\tif module.IsLoaded(id) {\n\t\treturn \"\", errors.New(\"a plugin with this id already exists\")\n\t}\n\n\tsource := strings.TrimPrefix(strings.TrimSuffix(body, \"}\"), \"{\")\n\n\tpluginSourceDir := PluginSourcePath + \"/\" + id\n\t_, err := os.Stat(pluginSourceDir)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn \"\", fmt.Errorf(\"error checking if directory exists: %s\", err)\n\t}\n\t// only create the directory if it doesn't exist; it's possible GPT had a compile\n\t// error in the last attempt in which case we can overwrite it\n\t//\n\t// we don't get this if the last attempt was successful since it'll show up\n\t// as a loaded plugin in the check above\n\tif os.IsNotExist(err) {\n\t\t// if err is nil then the directory doesn't exist, let's create it\n\t\terr := os.Mkdir(pluginSourceDir, 0777)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error creating directory: %s\", err)\n\t\t}\n\t}\n\n\tsourcePath := pluginSourceDir + \"/plugin.go\"\n\terr = ioutil.WriteFile(sourcePath, []byte(source), 0644)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error writing source file: %s\", err)\n\t}\n\n\tif m.cfg.IsSupervisedMode() {\n\t\tfmt.Println(\"============================================================\")\n\t\tfmt.Println()\n\t\tui.Warn(\"⚠️ GPT written plugins are untrusted code from the internet\")\n\t\tfmt.Println()\n\t\tfmt.Println(\"You should review this code before allowing it to be compiled and executed.\")\n\t\tfmt.Println()\n\t\tfmt.Println(\"If you allow this action, GPT is able to execute code with the same permissions as your user.\")\n\t\tfmt.Println()\n\t\tcolor.New(color.FgHiWhite, color.Bold).Println(\"This is potentially dangerous.\")\n\t\tfmt.Println()\n\t\tfmt.Println(\"The source code GPT has written can be found here:\")\n\t\tfmt.Println(sourcePath)\n\t\tfmt.Println()\n\t\tconfirmation := ui.PromptInput(\"Enter 'confirm' to confirm, anything else will block:\")\n\t\tif confirmation != \"confirm\" {\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(\"============================================================\")\n\t\t\treturn \"The user has prevented you from running this code\", errors.New(confirmation)\n\t\t}\n\t\tfmt.Println()\n\t\tfmt.Println(\"============================================================\")\n\t\tfmt.Println()\n\t}\n\n\tpluginPath := PluginCompilePath + \"/\" + id + \".so\"\n\tcmd := exec.Command(\"go\", \"build\", \"-buildmode=plugin\", \"-o\", pluginPath, sourcePath)\n\tif b, err := cmd.CombinedOutput(); err != nil {\n\t\treturn string(b), fmt.Errorf(\"error compiling plugin: %s\", err)\n\t}\n\n\tloadedPlugin, err := module.OpenPlugin(pluginPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error opening plugin: %s\", err)\n\t}\n\n\t// Call the functions provided by the plugin\n\tcompiledID := loadedPlugin.ID()\n\tif id != compiledID {\n\t\treturn \"\", errors.New(\"ID() does not return the ID specified in the '/plugin create <plugin-id>' command\")\n\t}\n\n\terr = module.LoadPlugin(module.GetModuleForPlugin(loadedPlugin))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error loading plugin: %s\", err)\n\t}\n\n\treturn `Great! Your plugin has been loaded successfully.\n\nWhy don't you check the /help command to see if your new plugin is available.'`, nil\n}\n\nvar newPluginPrompt = `You can add new plugins which you can call using a slash command.\n\nThey're written in Go, so all you need to do is create a new struct which implements the correct interface.\n\nThe interface you need to implement is:\n\n` + util.TripleQuote + `\ntype Plugin interface {\n\tExample() string\n\tExecute(input map[string]any) (map[string]any, error)\n}\n` + util.TripleQuote + `\n\nYou don't need to write any supporting code like the main function, you only need to implement the struct.\n\nHere's the full code for the \"add 1\" plugin you can use to guide your output:\n` + util.TripleQuote + `\npackage main\n\nimport \"github.com/ian-kent/gptchat/module\"\n\nvar Plugin module.Plugin = AddOne{}\n\ntype AddOne struct{}\n\nfunc (c AddOne) ID() string {\n\treturn \"add-one\"\n}\n\nfunc (c AddOne) Example() string {\n\treturn ` + util.SingleQuote + `/add-one {\n\t\"value\": 5\n}` + util.SingleQuote + `\n}\n\nfunc (c AddOne) Execute(input map[string]any) (map[string]any, error) {\n\tvalue, ok := input[\"value\"].(int)\n\tif !ok {\n\t\treturn nil, nil\n\t}\n\n\tvalue = value + 1\n\n\treturn map[string]any{\n\t\t\"result\": value,\n\t}, nil\n}\n` + util.TripleQuote + `\n\nIt's best if the plugins you create don't have any external dependencies. You can call external APIs if you want to, but you should avoid APIs which require authentication since you won't have the required access.\n\nYour plugin must import the module package and must define a package variable named 'Plugin', just like with the AddOne example. The result of the Execute function you implement must return either a value or an error.\n\nThe input to Execute is a map[string]any which you should assume is unmarshaled from JSON. This means you must use appropriate data types, for example a float64 when working with numbers.\n\nTo create a plugin, you should use the \"/plugin create <plugin-id> {}\" command, for example:\n\n` + util.TripleQuote + `\n/plugin create add-one {\n\tpackage main\n\n\t// the rest of your plugin source here\n}\n` + util.TripleQuote + `\n\nYour code inside the '/plugin create' body must be valid Go code which can compile without any errors. Do not include quotes or attempt to use a JSON body.`\n"
  },
  {
    "path": "module/plugin/source/README.md",
    "content": "Plugin source will be added to this directory."
  },
  {
    "path": "module/plugin.go",
    "content": "package module\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/gptchat/ui\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"os\"\n\t\"plugin\"\n\t\"strings\"\n)\n\ntype Plugin interface {\n\tID() string\n\tExample() string\n\tExecute(map[string]any) (map[string]any, error)\n}\n\ntype pluginLoader struct {\n\tplugin Plugin\n}\n\nfunc (p pluginLoader) Load(config.Config, *openai.Client) error {\n\treturn nil\n}\nfunc (p pluginLoader) UpdateConfig(config.Config) {}\nfunc (p pluginLoader) ID() string {\n\treturn p.plugin.ID()\n}\nfunc (p pluginLoader) Prompt() string {\n\treturn p.plugin.Example()\n}\nfunc (p pluginLoader) Execute(args, body string) (string, error) {\n\tinput := make(map[string]any)\n\tif body != \"\" {\n\t\terr := json.Unmarshal([]byte(body), &input)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"plugin body must be valid json: %s\", err)\n\t\t}\n\t}\n\n\toutput, err := p.plugin.Execute(input)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error executing plugin: %s\", err)\n\t}\n\n\tb, err := json.Marshal(output)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error converting plugin output to json: %s\", err)\n\t}\n\n\treturn string(b), nil\n}\n\nfunc GetModuleForPlugin(p Plugin) Module {\n\treturn pluginLoader{p}\n}\n\nfunc LoadCompiledPlugins() error {\n\tpluginPath := \"./module/plugin/compiled/\"\n\tentries, err := os.ReadDir(pluginPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error loading compiled plugins: %s\", err)\n\t}\n\n\tfor _, entry := range entries {\n\t\tif !strings.HasSuffix(entry.Name(), \".so\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tloadedPlugin, err := OpenPlugin(pluginPath + entry.Name())\n\t\tif err != nil {\n\t\t\tui.Warn(fmt.Sprintf(\"error opening plugin: %s\", err))\n\t\t\tcontinue\n\t\t}\n\n\t\tpluginID := loadedPlugin.ID()\n\t\tif IsLoaded(pluginID) {\n\t\t\tui.Warn(fmt.Sprintf(\"plugin with this ID is already loaded: %s\", err))\n\t\t\tcontinue\n\t\t}\n\n\t\terr = LoadPlugin(GetModuleForPlugin(loadedPlugin))\n\t\tif err != nil {\n\t\t\tui.Warn(fmt.Sprintf(\"error loading plugin: %s\", err))\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc OpenPlugin(path string) (Plugin, error) {\n\tp, err := plugin.Open(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error loading plugin: %s\", err)\n\t}\n\n\tapiSymbol, err := p.Lookup(\"Plugin\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error finding plugin implementation: %s\", err)\n\t}\n\n\t// Cast the symbol to the ScriptAPI interface\n\tapi, ok := apiSymbol.(*Plugin)\n\tif !ok {\n\t\treturn nil, errors.New(\"plugin does not implement the Plugin interface\")\n\t}\n\n\tloadedPlugin := *api\n\treturn loadedPlugin, nil\n}\n"
  },
  {
    "path": "parser/cmd/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/parser\"\n\t\"strings\"\n)\n\nfunc main() {\n\tinput := `/plugins create my-plugin\n{\n\tpackage main\n\n\timport \"fmt\"\n\n\tfunc main() {\n\t\tfmt.Println(\"test\")\n\t}\n}`\n\n\ttokens := parser.Lex(input)\n\n\tfmt.Println(\"Tokens:\")\n\tfor _, token := range tokens {\n\t\tfmt.Printf(\"    %20s => %s\\n\", token.Typ, token.Val)\n\t}\n\n\tfmt.Println()\n\n\tresult := parser.ParseTokens(tokens)\n\tfmt.Println(\"Result:\")\n\tfmt.Println(\"    Chat:\")\n\tfmt.Println(indent(result.Chat, \"        \"))\n\tfmt.Println(\"    Commands:\")\n\tfor _, command := range result.Commands {\n\t\tfmt.Printf(\"        - Command: %s\\n\", command.Command)\n\t\tfmt.Printf(\"        - Args: %s\\n\", command.Args)\n\t\tfmt.Printf(\"        - Body:\\n\")\n\t\tfmt.Println(indent(command.Body, \"              \"))\n\t}\n}\n\nfunc indent(input string, prefix string) string {\n\tlines := strings.Split(string(input), \"\\n\")\n\tvar output string\n\tfor _, line := range lines {\n\t\toutput += prefix + line + \"\\n\"\n\t}\n\treturn output\n}\n"
  },
  {
    "path": "parser/parser.go",
    "content": "package parser\n\nimport \"strings\"\n\ntype ParseResult struct {\n\tChat     string\n\tCommands []ParseCommand\n}\n\ntype ParseCommand struct {\n\tCommand string\n\tArgs    string\n\tBody    string\n}\n\nfunc (p ParseCommand) String() string {\n\toutput := p.Command\n\tif p.Args != \"\" {\n\t\toutput += \" \" + p.Args\n\t}\n\tif p.Body != \"\" {\n\t\toutput += \"\\n\" + p.Body\n\t}\n\treturn output\n}\n\nfunc Parse(input string) ParseResult {\n\ttokens := Lex(input)\n\treturn ParseTokens(tokens)\n}\n\ntype TokenType string\n\nconst (\n\tPlaintext TokenType = \"Plaintext\"\n\tNewline             = \"Newline\"\n\tCommand             = \"Command\"\n\tBody                = \"Body\"\n)\n\ntype Token struct {\n\tTyp TokenType\n\tVal string\n}\n\nfunc ParseTokens(tokens []Token) ParseResult {\n\tresult := ParseResult{}\n\tvar activeCommand *ParseCommand\n\tvar commands []*ParseCommand\n\tvar isInCommandContext bool\n\n\tfor _, token := range tokens {\n\t\tswitch token.Typ {\n\t\tcase Plaintext:\n\t\t\tif activeCommand == nil {\n\t\t\t\tresult.Chat += token.Val\n\t\t\t\tisInCommandContext = false\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif activeCommand.Args != \"\" {\n\t\t\t\t// ParseTokens error\n\t\t\t\tpanic(\"ParseTokens error: Command already has args\")\n\t\t\t}\n\t\t\tactiveCommand.Args = strings.TrimSpace(token.Val)\n\t\tcase Newline:\n\t\t\tif activeCommand != nil {\n\t\t\t\tactiveCommand = nil\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.HasSuffix(result.Chat, \"\\n\\n\") {\n\t\t\t\t// we don't append more than two consecutive newlines\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresult.Chat += token.Val\n\t\tcase Command:\n\t\t\tactiveCommand = &ParseCommand{Command: token.Val}\n\t\t\tcommands = append(commands, activeCommand)\n\t\t\tisInCommandContext = true\n\t\tcase Body:\n\t\t\tif activeCommand != nil {\n\t\t\t\tif activeCommand.Body != \"\" {\n\t\t\t\t\t// ParseTokens error\n\t\t\t\t\tpanic(\"ParseTokens error: Command already has Body\")\n\t\t\t\t}\n\t\t\t\tactiveCommand.Body = token.Val\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif isInCommandContext {\n\t\t\t\tlastCommand := commands[len(commands)-1]\n\t\t\t\tif lastCommand.Body != \"\" {\n\t\t\t\t\t// ParseTokens error\n\t\t\t\t\tpanic(\"ParseTokens error: Command already has Body\")\n\t\t\t\t}\n\t\t\t\tlastCommand.Body = token.Val\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tresult.Chat += token.Val\n\t\t}\n\t}\n\n\tresult.Chat = strings.TrimSpace(result.Chat)\n\tfor _, command := range commands {\n\t\tresult.Commands = append(result.Commands, *command)\n\t}\n\treturn result\n}\n\nfunc Lex(input string) []Token {\n\tvar tokens []Token\n\tvar currentToken *Token\n\tvar nesting int\n\n\tfor i, c := range input {\n\t\tswitch c {\n\t\tcase '/':\n\t\t\t// is this the start of a new Command?\n\t\t\tif i == 0 || input[i-1] == '\\n' {\n\t\t\t\tif currentToken != nil {\n\t\t\t\t\ttokens = append(tokens, *currentToken)\n\t\t\t\t}\n\t\t\t\tcurrentToken = &Token{Typ: Command, Val: \"/\"}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// if we have a Token, append to it\n\t\t\tif currentToken != nil {\n\t\t\t\tcurrentToken.Val += string(c)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// otherwise we can assume this is plain text\n\t\t\tcurrentToken = &Token{Typ: Plaintext, Val: \"/\"}\n\t\tcase ' ':\n\t\t\t// a space signifies the end of the Command\n\t\t\tif currentToken != nil && currentToken.Typ == Command {\n\t\t\t\ttokens = append(tokens, *currentToken)\n\t\t\t\tcurrentToken = nil\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// if we have a Token, append to it\n\t\t\tif currentToken != nil {\n\t\t\t\tcurrentToken.Val += string(c)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// otherwise we can assume this is plain text\n\t\t\tcurrentToken = &Token{Typ: Plaintext, Val: \" \"}\n\t\tcase '{':\n\t\t\t// if it's not already a Body, we'll store the current Token\n\t\t\tif currentToken != nil && currentToken.Typ != Body {\n\t\t\t\ttokens = append(tokens, *currentToken)\n\t\t\t}\n\t\t\t// If we already have a body, we'll add to it\n\t\t\tif currentToken != nil && currentToken.Typ == Body {\n\t\t\t\tnesting++\n\t\t\t\tcurrentToken.Val += string(c)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Otherwise we'll start a new body\n\t\t\tcurrentToken = &Token{Typ: Body, Val: \"{\"}\n\t\t\tnesting++\n\t\tcase '}':\n\t\t\t// if we're already in a Body, the Body ends\n\t\t\tif currentToken != nil && currentToken.Typ == Body {\n\t\t\t\tnesting--\n\t\t\t\tcurrentToken.Val += \"}\"\n\n\t\t\t\tif nesting == 0 {\n\t\t\t\t\ttokens = append(tokens, *currentToken)\n\t\t\t\t\tcurrentToken = nil\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// if we have a Token, append to it\n\t\t\tif currentToken != nil {\n\t\t\t\tcurrentToken.Val += string(c)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// otherwise we can assume this is plain text\n\t\t\tcurrentToken = &Token{Typ: Plaintext, Val: \" \"}\n\t\tcase '\\n':\n\t\t\t// if we have Plaintext or a Body, we'll append to it\n\t\t\tif currentToken != nil && (currentToken.Typ == Body) {\n\t\t\t\tcurrentToken.Val += \"\\n\"\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// otherwise we always end the current Token at a new line\n\t\t\tif currentToken != nil {\n\t\t\t\ttokens = append(tokens, *currentToken)\n\t\t\t\tcurrentToken = nil\n\t\t\t}\n\n\t\t\t// and we store a new line Token\n\t\t\ttokens = append(tokens, Token{Typ: Newline, Val: \"\\n\"})\n\t\tdefault:\n\t\t\tif currentToken == nil {\n\t\t\t\tcurrentToken = &Token{Typ: Plaintext}\n\t\t\t}\n\t\t\tcurrentToken.Val += string(c)\n\t\t}\n\t}\n\tif currentToken != nil {\n\t\ttokens = append(tokens, *currentToken)\n\t}\n\n\treturn tokens\n}\n"
  },
  {
    "path": "parser/parser_test.go",
    "content": "package parser\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"testing\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttestCases := []struct {\n\t\tname   string\n\t\tinput  string\n\t\toutput ParseResult\n\t}{\n\t\t{\n\t\t\tname:  \"basic Command\",\n\t\t\tinput: \"/api get /path\",\n\t\t\toutput: ParseResult{\n\t\t\t\tChat: \"\",\n\t\t\t\tCommands: []ParseCommand{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand: \"/api\",\n\t\t\t\t\t\tArgs:    \"get /path\",\n\t\t\t\t\t\tBody:    \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"Command with args\",\n\t\t\tinput: \"/api get /path { something }\",\n\t\t\toutput: ParseResult{\n\t\t\t\tChat: \"\",\n\t\t\t\tCommands: []ParseCommand{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand: \"/api\",\n\t\t\t\t\t\tArgs:    \"get /path\",\n\t\t\t\t\t\tBody:    \"{ something }\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiline Command with args\",\n\t\t\tinput: `/api get /path {\n\tsomething\n}`,\n\t\t\toutput: ParseResult{\n\t\t\t\tChat: \"\",\n\t\t\t\tCommands: []ParseCommand{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand: \"/api\",\n\t\t\t\t\t\tArgs:    \"get /path\",\n\t\t\t\t\t\tBody: `{\n\tsomething\n}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiline Command with args\",\n\t\t\tinput: `/api get /path\n{\n\tsomething\n}`,\n\t\t\toutput: ParseResult{\n\t\t\t\tChat: \"\",\n\t\t\t\tCommands: []ParseCommand{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand: \"/api\",\n\t\t\t\t\t\tArgs:    \"get /path\",\n\t\t\t\t\t\tBody: `{\n\tsomething\n}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"chat with multiline Command with args\",\n\t\t\tinput: `This is some chat\n\n/api get /path\n{\n\tsomething\n}`,\n\t\t\toutput: ParseResult{\n\t\t\t\tChat: \"This is some chat\",\n\t\t\t\tCommands: []ParseCommand{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand: \"/api\",\n\t\t\t\t\t\tArgs:    \"get /path\",\n\t\t\t\t\t\tBody: `{\n\tsomething\n}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"chat with multiple multiline Command with args\",\n\t\t\tinput: `This is some chat\n\n/api get /path\n{\n\tsomething\n}\n\nThis is some more chat\n\n/api post /another-path\n{\n\tsomething else\n}`,\n\t\t\toutput: ParseResult{\n\t\t\t\tChat: \"This is some chat\\n\\nThis is some more chat\",\n\t\t\t\tCommands: []ParseCommand{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand: \"/api\",\n\t\t\t\t\t\tArgs:    \"get /path\",\n\t\t\t\t\t\tBody: `{\n\tsomething\n}`,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand: \"/api\",\n\t\t\t\t\t\tArgs:    \"post /another-path\",\n\t\t\t\t\t\tBody: `{\n\tsomething else\n}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiline Command with code Body\",\n\t\t\tinput: `/plugins create my-plugin\n{\n\tpackage main\n\n\timport \"fmt\"\n\n\tfunc main() {\n\t\tfmt.Println(\"test\")\n\t}\n}`,\n\t\t\toutput: ParseResult{\n\t\t\t\tChat: \"\",\n\t\t\t\tCommands: []ParseCommand{\n\t\t\t\t\t{\n\t\t\t\t\t\tCommand: \"/plugins\",\n\t\t\t\t\t\tArgs:    \"create my-plugin\",\n\t\t\t\t\t\tBody: `{\n\tpackage main\n\n\timport \"fmt\"\n\n\tfunc main() {\n\t\tfmt.Println(\"test\")\n\t}\n}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\ttokens := Lex(testCase.input)\n\t\t\tast := ParseTokens(tokens)\n\t\t\tassert.Equal(t, testCase.output, ast)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "ui/theme.go",
    "content": "package ui\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar theme Theme\n\nfunc init() {\n\tif strings.ToUpper(os.Getenv(\"GPTCHAT_THEME\")) == \"DARK\" {\n\t\ttheme = DarkTheme\n\t} else {\n\t\ttheme = LightTheme\n\t}\n}\n\ntype Theme struct {\n\tUsername *color.Color\n\tMessage  *color.Color\n\tUseful   *color.Color\n\tAI       *color.Color\n\tUser     *color.Color\n\tInfo     *color.Color\n\tError    *color.Color\n\tWarn     *color.Color\n\tApp      *color.Color\n\tAppBold  *color.Color\n}\n\nvar LightTheme = Theme{\n\tUsername: color.New(color.FgRed),\n\tMessage:  color.New(color.FgBlue),\n\tUseful:   color.New(color.FgWhite),\n\tAI:       color.New(color.FgGreen),\n\tUser:     color.New(color.FgYellow),\n\tInfo:     color.New(color.FgWhite, color.Bold),\n\tError:    color.New(color.FgHiRed, color.Bold),\n\tWarn:     color.New(color.FgHiYellow, color.Bold),\n\tApp:      color.New(color.FgWhite),\n\tAppBold:  color.New(color.FgGreen, color.Bold),\n}\n\nvar DarkTheme = Theme{\n\tUsername: color.New(color.FgRed),\n\tMessage:  color.New(color.FgBlue),\n\tUseful:   color.New(color.FgBlack),\n\tAI:       color.New(color.FgGreen),\n\tUser:     color.New(color.FgMagenta),\n\tInfo:     color.New(color.FgBlack, color.Bold),\n\tError:    color.New(color.FgHiRed, color.Bold),\n\tWarn:     color.New(color.FgHiMagenta, color.Bold),\n\tApp:      color.New(color.FgBlack),\n\tAppBold:  color.New(color.FgGreen, color.Bold),\n}\n"
  },
  {
    "path": "ui/ui.go",
    "content": "package ui\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nconst (\n\tUser   = \"USER\"\n\tAI     = \"AI\"\n\tSystem = \"SYSTEM\"\n\tTool   = \"TOOL\"\n\tAPI    = \"API\"\n\tModule = \"MODULE\"\n\tApp    = \"APP\"\n)\n\nfunc Error(message string, err error) {\n\ttheme.Error.Printf(\"ERROR: \")\n\ttheme.Useful.Printf(\"%s: %v\\n\\n\", message, err)\n}\n\nfunc Warn(message string) {\n\ttheme.Warn.Printf(\"WARNING: \")\n\ttheme.Useful.Printf(\"%s\\n\", message)\n}\n\nfunc Info(message string) {\n\ttheme.Warn.Printf(\"INFO: \")\n\ttheme.Useful.Printf(\"%s\\n\", message)\n}\n\nfunc Welcome(title, message string) {\n\ttheme.AppBold.Printf(\"%s\\n\\n\", title)\n\ttheme.App.Printf(\"%s\\n\\n\", message)\n}\n\nfunc PrintChatDebug(name, message string) {\n\ttheme.Useful.Printf(\"[DEBUG] \")\n\tPrintChat(name, message)\n}\n\nfunc PrintChat(name, message string) {\n\tswitch name {\n\tcase User:\n\t\ttheme.User.Printf(\"%s:\\n\\n\", name)\n\t\ttheme.Message.Printf(\"%s\\n\", indent(message))\n\tcase AI:\n\t\ttheme.AI.Printf(\"%s:\\n\\n\", name)\n\t\ttheme.Useful.Printf(\"%s\\n\", indent(message))\n\tcase App:\n\t\ttheme.AppBold.Printf(\"%s:\\n\\n\", name)\n\t\ttheme.Useful.Printf(\"%s\\n\", indent(message))\n\tcase System:\n\t\tfallthrough\n\tcase Tool:\n\t\tfallthrough\n\tcase API:\n\t\tfallthrough\n\tcase Module:\n\t\tfallthrough\n\tdefault:\n\t\ttheme.Username.Printf(\"%s:\\n\\n\", name)\n\t\ttheme.Message.Printf(\"%s\\n\", indent(message))\n\t}\n}\n\nfunc PromptChatInput() string {\n\treader := bufio.NewReader(os.Stdin)\n\ttheme.User.Printf(\"USER:\\n\\n    \")\n\ttext, _ := reader.ReadString('\\n')\n\ttext = strings.TrimSpace(text)\n\tfmt.Println()\n\n\treturn text\n}\n\nfunc PromptConfirm(prompt string) bool {\n\treader := bufio.NewReader(os.Stdin)\n\ttheme.AppBold.Printf(\"%s [Y/N]: \", prompt)\n\ttext, _ := reader.ReadString('\\n')\n\ttext = strings.TrimSpace(text)\n\tfmt.Println()\n\n\treturn strings.ToUpper(text) == \"Y\"\n}\n\nfunc PromptInput(prompt string) string {\n\treader := bufio.NewReader(os.Stdin)\n\ttheme.AppBold.Printf(\"%s \", prompt)\n\ttext, _ := reader.ReadString('\\n')\n\ttext = strings.TrimSpace(text)\n\treturn text\n}\n\nfunc indent(input string) string {\n\tlines := strings.Split(string(input), \"\\n\")\n\tvar output string\n\tfor _, line := range lines {\n\t\toutput += \"    \" + line + \"\\n\"\n\t}\n\treturn output\n}\n"
  },
  {
    "path": "util/strings.go",
    "content": "package util\n\nconst SingleQuote = \"`\"\nconst TripleQuote = \"```\"\n"
  }
]