commit d1b4cf5672eea58630b0e198e06f749c0d70de3a Author: asrul10 Date: Sat Mar 11 20:22:34 2023 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79f6b07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work +*.log +lcg + diff --git a/README.md b/README.md new file mode 100644 index 0000000..71d5586 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +## Linux Command GPT (lcg) +Get Linux commands in natural language with the power of ChatGPT. + +### Installation +Build from source +```bash +> git clone --depth 1 https://github.com/asrul10/linux-command-gpt.git ~/.linux-command-gpt +> cd linux-command-gpt +> go build -o lcg +# Add to your environment $PATH +> ln -s ~/.local/bin/fd +``` + +### Example Usage + +```bash +> lcg I want to extract file linux-command-gpt.tar.gz +Completed in 0.92 seconds +┌────────────────────────────────────┐ +│ tar -xvzf linux-command-gpt.tar.gz │ +└────────────────────────────────────┘ +Are you sure you want to execute the command? (Y/n): +``` + +### Options +```bash +> gpt3 [options] + +--help output usage information +--version output the version number +--update-key update the API key +--delete-key delete the API key +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..34864fe --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/asrul/linux-command-gpt + +go 1.18 diff --git a/gpt/gpt.go b/gpt/gpt.go new file mode 100644 index 0000000..07edd4b --- /dev/null +++ b/gpt/gpt.go @@ -0,0 +1,173 @@ +package gpt + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" +) + +type Gpt3 struct { + CompletionUrl string + Prompt string + Model string + HomeDir string + ApiKeyFile string + ApiKey string +} + +type Chat struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type Gpt3Request struct { + Model string `json:"model"` + Messages []Chat `json:"messages"` +} + +type Gpt3Response struct { + Choices []struct { + Message Chat `json:"message"` + } `json:"choices"` +} + +func (gpt3 *Gpt3) deleteApiKey() { + filePath := gpt3.HomeDir + string(filepath.Separator) + gpt3.ApiKeyFile + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return + } + err := os.Remove(filePath) + if err != nil { + panic(err) + } +} + +func (gpt3 *Gpt3) updateApiKey(apiKey string) { + filePath := gpt3.HomeDir + string(filepath.Separator) + gpt3.ApiKeyFile + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + panic(err) + } + defer file.Close() + + apiKey = strings.TrimSpace(apiKey) + _, err = file.WriteString(apiKey) + if err != nil { + panic(err) + } + gpt3.ApiKey = apiKey +} + +func (gpt3 *Gpt3) storeApiKey(apiKey string) { + if apiKey == "" { + return + } + filePath := gpt3.HomeDir + string(filepath.Separator) + gpt3.ApiKeyFile + file, err := os.Create(filePath) + if err != nil { + panic(err) + } + defer file.Close() + + apiKey = strings.TrimSpace(apiKey) + _, err = file.WriteString(apiKey) + if err != nil { + panic(err) + } + gpt3.ApiKey = apiKey +} + +func (gpt3 *Gpt3) loadApiKey() bool { + dirSeparator := string(filepath.Separator) + apiKeyFile := gpt3.HomeDir + dirSeparator + gpt3.ApiKeyFile + if _, err := os.Stat(apiKeyFile); os.IsNotExist(err) { + return false + } + apiKey, err := ioutil.ReadFile(apiKeyFile) + if err != nil { + return false + } + gpt3.ApiKey = string(apiKey) + + return true +} + +func (gpt3 *Gpt3) UpdateKey() { + var apiKey string + fmt.Print("OpenAI API Key: ") + fmt.Scanln(&apiKey) + gpt3.updateApiKey(apiKey) +} + +func (gpt3 *Gpt3) DeleteKey() { + var c string + fmt.Print("Are you sure you want to delete the API key? (y/N): ") + fmt.Scanln(&c) + if c == "Y" || c == "y" { + gpt3.deleteApiKey() + } +} + +func (gpt3 *Gpt3) InitKey() { + load := gpt3.loadApiKey() + if load { + return + } + var apiKey string + fmt.Print("OpenAI API Key: ") + fmt.Scanln(&apiKey) + gpt3.storeApiKey(apiKey) +} + +func (gpt3 *Gpt3) Completions(ask string) string { + req, err := http.NewRequest("POST", gpt3.CompletionUrl, nil) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(gpt3.ApiKey)) + + messages := []Chat{ + {"system", gpt3.Prompt}, + {"user", ask}, + } + payload := Gpt3Request{ + gpt3.Model, + messages, + } + payloadJson, err := json.Marshal(payload) + if err != nil { + panic(err) + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(payloadJson)) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + if resp.StatusCode != http.StatusOK { + fmt.Println(string(body)) + return "" + } + + var res Gpt3Response + err = json.Unmarshal(body, &res) + if err != nil { + panic(err) + } + + return strings.TrimSpace(res.Choices[0].Message.Content) +} diff --git a/gpt/gpt_test.go b/gpt/gpt_test.go new file mode 100644 index 0000000..61c849a --- /dev/null +++ b/gpt/gpt_test.go @@ -0,0 +1,58 @@ +package gpt + +import ( + "testing" +) + +func TestApiKey(t *testing.T) { + gpt3 := Gpt3{ + ApiKeyFile: ".openai_api_key_test", + } + + tests := []struct { + homeDir string + apiKey string + expected bool + expectedApiKey string + }{ + {".", "", false, ""}, + {"./", "", false, ""}, + {".", "the key 123", true, "the key 123"}, + {".", "the key 123\n", true, "the key 123"}, + {".", " the key 123 ", true, "the key 123"}, + {".", " \n\n the key 123 \n\n", true, "the key 123"}, + } + defer gpt3.deleteApiKey() + + for _, test := range tests { + gpt3.HomeDir = test.homeDir + gpt3.storeApiKey(test.apiKey) + load := gpt3.loadApiKey() + gpt3.deleteApiKey() + if load != test.expected { + t.Error("Expected load to be", test.expected, "got", load) + } + if gpt3.ApiKey != test.expectedApiKey { + t.Error("Expected ApiKey to be", test.expectedApiKey, "got", gpt3.ApiKey) + } + } + + // Test update api key + gpt3.HomeDir = "." + gpt3.storeApiKey("test") + updateTests := []struct { + apiKey string + expectedApiKey string + }{ + {"the key 123", "the key 123"}, + {"the key 123\n", "the key 123"}, + {" the key 123 ", "the key 123"}, + {" \n\n the key 123 \n\n", "the key 123"}, + } + for _, test := range updateTests { + gpt3.updateApiKey(test.apiKey) + if gpt3.ApiKey != test.expectedApiKey { + t.Error("Expected ApiKey to be", test.expectedApiKey, "got", gpt3.ApiKey) + } + } +} diff --git a/gpt/linux-command-gpt b/gpt/linux-command-gpt new file mode 100755 index 0000000..566f1f4 Binary files /dev/null and b/gpt/linux-command-gpt differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..de2cac7 --- /dev/null +++ b/main.go @@ -0,0 +1,148 @@ +package main + +import ( + "fmt" + "math" + "os" + "os/exec" + "os/user" + "strings" + "time" + + "github.com/asrul/linux-command-gpt/gpt" +) + +const ( + HOST = "https://api.openai.com/v1/" + COMPLETIONS = "chat/completions" + MODEL = "gpt-3.5-turbo" + PROMPT = "I want you to reply with linux command and nothing else. Do not write explanations." + + // This file is created in the user's home directory + // Example: /home/username/.openai_api_key + API_KEY_FILE = ".openai_api_key" + + HELP = ` + +Usage: gpt3 [options] + + --help output usage information + --version output the version number + --update-key update the API key + --delete-key delete the API key + + ` + + VERSION = "0.1.0" + CMD_HELP = 100 + CMD_VERSION = 101 + CMD_UPDATE = 102 + CMD_DELETE = 103 + CMD_COMPLETION = 110 +) + +func handleCommand(cmd string) int { + if cmd == "" || cmd == "--help" || cmd == "-h" { + return CMD_HELP + } + if cmd == "--version" || cmd == "-v" { + return CMD_VERSION + } + if cmd == "--update-key" || cmd == "-u" { + return CMD_UPDATE + } + if cmd == "--delete-key" || cmd == "-d" { + return CMD_DELETE + } + return CMD_COMPLETION +} + +func main() { + currentUser, err := user.Current() + if err != nil { + panic(err) + } + + args := os.Args + cmd := "" + if len(args) > 1 { + cmd = strings.Join(args[1:], " ") + } + h := handleCommand(cmd) + + if h == CMD_HELP { + fmt.Println(HELP) + return + } + + if h == CMD_VERSION { + fmt.Println(VERSION) + return + } + + gpt3 := gpt.Gpt3{ + CompletionUrl: HOST + COMPLETIONS, + Model: MODEL, + Prompt: PROMPT, + HomeDir: currentUser.HomeDir, + ApiKeyFile: API_KEY_FILE, + } + + if h == CMD_UPDATE { + gpt3.UpdateKey() + return + } + + if h == CMD_DELETE { + gpt3.DeleteKey() + return + } + + s := time.Now() + done := make(chan bool) + go func() { + loadingChars := []rune{'-', '\\', '|', '/'} + i := 0 + for { + select { + case <-done: + fmt.Printf("\r") + return + default: + fmt.Printf("\rLoading %c", loadingChars[i]) + i = (i + 1) % len(loadingChars) + time.Sleep(30 * time.Millisecond) + } + } + }() + + gpt3.InitKey() + r := gpt3.Completions(cmd) + done <- true + if r == "" { + return + } + + c := "Y" + elapsed := time.Since(s).Seconds() + elapsed = math.Round(elapsed*100) / 100 + fmt.Printf("Completed in %v seconds\n", elapsed) + fmt.Printf("┌%s┐\n", strings.Repeat("─", len(r)+2)) + fmt.Printf("│ %s │\n", r) + fmt.Printf("└%s┘\n", strings.Repeat("─", len(r)+2)) + fmt.Print("Are you sure you want to execute the command? (Y/n): ") + fmt.Scanln(&c) + if c != "Y" && c != "y" { + return + } + + cmsplit := strings.Split(r, " ") + cm := exec.Command(cmsplit[0], cmsplit[1:]...) + out, err := cm.Output() + if err != nil { + fmt.Println(err.Error()) + return + } + + fmt.Println(string(out)) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..d72f4e7 --- /dev/null +++ b/main_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "testing" +) + +func TestHandleCommand(t *testing.T) { + tests := []struct { + command string + expected int + }{ + {"", CMD_HELP}, + {"--help", CMD_HELP}, + {"-h", CMD_HELP}, + {"--version", CMD_VERSION}, + {"-v", CMD_VERSION}, + {"--update-key", CMD_UPDATE}, + {"-u", CMD_UPDATE}, + {"--delete-key", CMD_DELETE}, + {"-d", CMD_DELETE}, + {"random strings", CMD_COMPLETION}, + {"--test", CMD_COMPLETION}, + {"-test", CMD_COMPLETION}, + {"how to extract test.tar.gz", CMD_COMPLETION}, + } + + for _, test := range tests { + result := handleCommand(test.command) + if result != test.expected { + t.Error("Expected", test.expected, "got", result) + } + } +}