first commit

This commit is contained in:
asrul10
2023-03-11 20:22:34 +07:00
commit d1b4cf5672
8 changed files with 459 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
go.work
*.log
lcg

33
README.md Normal file
View File

@@ -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
```

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/asrul/linux-command-gpt
go 1.18

173
gpt/gpt.go Normal file
View File

@@ -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)
}

58
gpt/gpt_test.go Normal file
View File

@@ -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)
}
}
}

BIN
gpt/linux-command-gpt Executable file

Binary file not shown.

148
main.go Normal file
View File

@@ -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))
}

33
main_test.go Normal file
View File

@@ -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)
}
}
}