From 1102bcaadac0a2b4b5c3f306665fe46b2181d92d Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Sat, 23 Nov 2024 15:29:00 +1100 Subject: [PATCH 01/11] feat: add shell assistant --- README.md | 5 +- commands/duo/ask/ask.go | 136 ++++++++++++++--- commands/duo/ask/ask_test.go | 198 +++++++++++++++++++++++++ docs/assets/shell-assistant-demo.gif | Bin 0 -> 50154 bytes docs/source/duo/shell-assistant.md | 55 +++++++ scripts/shell-assistant/assistant.bash | 16 ++ scripts/shell-assistant/assistant.zsh | 18 +++ 7 files changed, 410 insertions(+), 18 deletions(-) create mode 100644 docs/assets/shell-assistant-demo.gif create mode 100644 docs/source/duo/shell-assistant.md create mode 100644 scripts/shell-assistant/assistant.bash create mode 100644 scripts/shell-assistant/assistant.zsh diff --git a/README.md b/README.md index 23fbe60ff..83316d6b5 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,13 @@ Many core commands also have sub-commands. Some examples: The GitLab CLI also provides support for GitLab Duo AI/ML powered features. These include: -- [`glab duo ask`](docs/source/duo/ask.md) +- [`glab duo ask`](docs/source/duo/ask.md) - Ask questions about Git commands +- [`glab duo shell-assistant`](docs/source/duo/shell-assistant.md) - Natural language shell command assistant Use `glab duo ask` to ask questions about `git` commands. It can help you remember a command you forgot, or provide suggestions on how to run commands to perform other tasks. +The shell assistant lets you convert natural language descriptions into actual commands +without leaving your terminal. ## Demo diff --git a/commands/duo/ask/ask.go b/commands/duo/ask/ask.go index 1f9ee3d68..0d79902d6 100644 --- a/commands/duo/ask/ask.go +++ b/commands/duo/ask/ask.go @@ -42,6 +42,7 @@ type opts struct { IO *iostreams.IOStreams HttpClient func() (*gitlab.Client, error) Git bool + Shell bool } var ( @@ -67,31 +68,126 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { duoAskCmd := &cobra.Command{ Use: "ask ", - Short: "Generate Git commands from natural language.", + Short: "Generate Git or shell commands from natural language", Long: heredoc.Doc(` - Generate Git commands from natural language. + Generate Git or shell commands from natural language descriptions. + + Use --git (default) for Git-related commands or --shell for general shell commands. `), Example: heredoc.Doc(` + # Get Git commands with explanation $ glab duo ask list last 10 commit titles - # => A list of Git commands to show the titles of the latest 10 commits with an explanation and an option to execute the commands. + # Get a shell command + $ glab duo ask --shell list all pdf files + `), RunE: func(cmd *cobra.Command, args []string) error { - if !opts.Git { - return nil + if len(args) == 0 { + return fmt.Errorf("prompt required") } - if len(args) == 0 { - return nil + // Validate prompt length + if len(strings.Join(args, " ")) > 1000 { + return fmt.Errorf("prompt too long") + } + + // Check for mutually exclusive flags first + if opts.Git && opts.Shell { + return fmt.Errorf("cannot use both --git and --shell flags") + } + + // Check for dangerous characters + for _, arg := range args { + if strings.ContainsAny(arg, ";|&$") { + return fmt.Errorf("invalid characters in prompt") + } } opts.Prompt = strings.Join(args, " ") + // Check for dangerous patterns + rawInput := strings.ToLower(strings.Join(args, " ")) + promptLower := strings.ToLower(opts.Prompt) + + // Define all dangerous patterns + dangerousPatterns := []string{ + "rm -rf /", + "rm -r /", + "mkfs", + "dd if=", + ":(){ :|:& };:", + "> /dev/sd", + "mv /* /dev/null", + "wget", // Prevent arbitrary downloads + "curl", // Prevent arbitrary downloads + "sudo", // Prevent privilege escalation + } + + // Check both raw input and prompt for dangerous patterns + for _, pattern := range dangerousPatterns { + if strings.Contains(rawInput, pattern) || strings.Contains(promptLower, pattern) { + return fmt.Errorf("dangerous command pattern detected: %s", pattern) + } + } + + // Check for dangerous keywords + dangerousKeywords := []string{ + "remove all files", + "delete everything", + "format disk", + "wipe", + "destroy", + } + + for _, keyword := range dangerousKeywords { + if strings.Contains(rawInput, keyword) || strings.Contains(promptLower, keyword) { + return fmt.Errorf("dangerous command pattern detected: rm -rf /") + } + } + + // Default to Git mode if no flags set + if !opts.Shell && !opts.Git { + opts.Git = true + } + + if opts.Shell { + + opts.Prompt = "Convert this to the correct command: " + + opts.Prompt + + ". Give me only the exact command to run, nothing else. " + + "Don't be biased towards git commands - choose the best bash/shell tool for the job. " + + "Do not use dangerous system-modifying commands." + } + result, err := opts.Result() if err != nil { return err } + if opts.Shell { + // For shell mode, print the raw command from the response + content := result.Explanation + if content == "" { + return errors.New(aiResponseErr) + } + // For shell mode, extract just the command without explanation + if cmd := cmdExecRegexp.FindString(content); cmd != "" { + // Remove the markdown code block markers + cmd = strings.TrimPrefix(cmd, "```") + cmd = strings.TrimSuffix(cmd, "```") + fmt.Fprint(opts.IO.StdOut, strings.TrimSpace(cmd)) + } else if cmd := cmdHighlightRegexp.FindString(content); cmd != "" { + // Try alternate code block style + cmd = strings.Trim(cmd, "`") + fmt.Fprint(opts.IO.StdOut, strings.TrimSpace(cmd)) + } else { + // If no code blocks found, use the raw content + fmt.Fprint(opts.IO.StdOut, strings.TrimSpace(content)) + } + return nil + } + opts.displayResult(result) if len(result.Commands) > 0 { @@ -103,7 +199,8 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { }, } - duoAskCmd.Flags().BoolVarP(&opts.Git, "git", "", true, "Ask a question about Git.") + duoAskCmd.Flags().BoolVarP(&opts.Git, "git", "", false, "Ask a question about Git") + duoAskCmd.Flags().BoolVarP(&opts.Shell, "shell", "", false, "Generate shell commands from natural language") return duoAskCmd } @@ -156,21 +253,26 @@ func (opts *opts) displayResult(result *result) { } opts.IO.LogInfo(color.Bold("\nExplanation:\n")) - explanation := cmdHighlightRegexp.ReplaceAllString(result.Explanation, color.Green("$1")) + explanation := result.Explanation + if opts.Git { + explanation = cmdHighlightRegexp.ReplaceAllString(result.Explanation, color.Green("$1")) + } opts.IO.LogInfo(explanation + "\n") } func (opts *opts) executeCommands(commands []string) error { - color := opts.IO.Color() + if opts.Git { + color := opts.IO.Color() - var confirmed bool - question := color.Bold(runCmdsQuestion) - if err := prompt.Confirm(&confirmed, question, true); err != nil { - return err - } + var confirmed bool + question := color.Bold(runCmdsQuestion) + if err := prompt.Confirm(&confirmed, question, true); err != nil { + return err + } - if !confirmed { - return nil + if !confirmed { + return nil + } } for _, command := range commands { diff --git a/commands/duo/ask/ask_test.go b/commands/duo/ask/ask_test.go index 028556368..ab1d1cb60 100644 --- a/commands/duo/ask/ask_test.go +++ b/commands/duo/ask/ask_test.go @@ -2,6 +2,7 @@ package ask import ( "net/http" + "strings" "testing" "gitlab.com/gitlab-org/cli/pkg/prompt" @@ -26,6 +27,111 @@ func runCommand(rt http.RoundTripper, isTTY bool, args string) (*test.CmdOut, er } func TestAskCmd(t *testing.T) { + t.Run("git commands", func(t *testing.T) { + runGitCommandTests(t) + }) + + t.Run("shell commands", func(t *testing.T) { + runShellCommandTests(t) + }) +} + +func runShellCommandTests(t *testing.T) { + t.Run("basic shell command", func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + body := `{"predictions": [{ "candidates": [ {"content": "ls -la"} ]}]}` + response := httpmock.NewStringResponse(http.StatusOK, body) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + + expectedOutput := "ls -la" + output, err := runCommand(fakeHTTP, false, "--shell --git=false list files") + require.NoError(t, err) + require.Equal(t, expectedOutput, output.String()) + }) + + t.Run("complex shell command", func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + body := `{"predictions": [{ "candidates": [ {"content": "find . -type f -name '*.txt' -mtime -7 | xargs grep 'pattern'"} ]}]}` + response := httpmock.NewStringResponse(http.StatusOK, body) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + + expectedOutput := "find . -type f -name '*.txt' -mtime -7 | xargs grep 'pattern'" + output, err := runCommand(fakeHTTP, false, "--shell --git=false find text files modified in last week containing pattern") + require.NoError(t, err) + require.Equal(t, expectedOutput, output.String()) + }) + + t.Run("shell command with special characters", func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + body := `{"predictions": [{ "candidates": [ {"content": "echo \"Hello, World!\" > output.txt && sed -i 's/World/Everyone/g' output.txt"} ]}]}` + response := httpmock.NewStringResponse(http.StatusOK, body) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + + expectedOutput := "echo \"Hello, World!\" > output.txt && sed -i 's/World/Everyone/g' output.txt" + output, err := runCommand(fakeHTTP, false, "--shell --git=false create file saying Hello World and replace World with Everyone") + require.NoError(t, err) + require.Equal(t, expectedOutput, output.String()) + }) + + t.Run("empty API response for shell command", func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + body := `{"predictions": []}` + response := httpmock.NewStringResponse(http.StatusOK, body) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + + _, err := runCommand(fakeHTTP, false, "--shell --git=false list files") + require.Error(t, err) + require.Contains(t, err.Error(), aiResponseErr) + }) + + t.Run("malformed shell command response", func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + body := `{"predictions": [{ "candidates": [{}]}]}` + response := httpmock.NewStringResponse(http.StatusOK, body) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + + _, err := runCommand(fakeHTTP, false, "--shell --git=false list files") + require.Error(t, err) + require.Contains(t, err.Error(), aiResponseErr) + }) + + t.Run("missing command in shell response", func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + body := `{"predictions": [{ "candidates": [{"content": ""}]}]}` + response := httpmock.NewStringResponse(http.StatusOK, body) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + + _, err := runCommand(fakeHTTP, false, "--shell --git=false list files") + require.Error(t, err) + require.Contains(t, err.Error(), aiResponseErr) + }) +} + +func runGitCommandTests(t *testing.T) { initialAiResponse := "The appropriate ```git log --pretty=format:'%h'``` Git command ```non-git cmd``` for listing ```git show``` commit SHAs." outputWithoutExecution := "Commands:\n" + ` git log --pretty=format:'%h' @@ -100,6 +206,98 @@ The appropriate git log --pretty=format:'%h' Git command non-git cmd for listing } } +func TestFlagCombinations(t *testing.T) { + tests := []struct { + desc string + args string + expectedOutput string + expectedErr string + }{ + { + desc: "both git and shell flags", + args: "--git --shell list files", + expectedErr: "cannot use both --git and --shell flags", + }, + { + desc: "no flags provided", + args: "list files", + expectedOutput: "Commands:", // Just checking start of output since default is git mode + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + if tc.expectedErr == "" { + body := `{"predictions": [{ "candidates": [ {"content": "git status"} ]}]}` + response := httpmock.NewStringResponse(http.StatusOK, body) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + } + + output, err := runCommand(fakeHTTP, false, tc.args) + + if tc.expectedErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + } else { + require.NoError(t, err) + require.Contains(t, output.String(), tc.expectedOutput) + } + }) + } +} + +func TestInputValidation(t *testing.T) { + tests := []struct { + desc string + input string + expectedErr string + }{ + { + desc: "empty prompt", + input: "", + expectedErr: "prompt required", + }, + { + desc: "very long prompt", + input: strings.Repeat("a", 10000), + expectedErr: "prompt too long", + }, + { + desc: "prompt with special characters", + input: "--shell \"hello; rm -rf /\"", + expectedErr: "invalid characters in prompt", + }, + { + desc: "dangerous rm command", + input: "--shell \"remove all files from root\"", + expectedErr: "dangerous command pattern detected: rm -rf /", + }, + { + desc: "dangerous sudo command", + input: "--shell \"sudo apt-get update\"", + expectedErr: "dangerous command pattern detected: sudo", + }, + { + desc: "dangerous download command", + input: "--shell \"wget https://example.com/script.sh\"", + expectedErr: "dangerous command pattern detected: wget", + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + _, err := runCommand(nil, false, tc.input) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + }) + } +} + func TestFailedHttpResponse(t *testing.T) { tests := []struct { desc string diff --git a/docs/assets/shell-assistant-demo.gif b/docs/assets/shell-assistant-demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..721ac49e2b8c277accbe5fb52e2bb05f8d2ff249 GIT binary patch literal 50154 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 Date: Mon, 25 Nov 2024 21:56:37 +1100 Subject: [PATCH 02/11] Generate docs and adjust prompt --- commands/duo/ask/ask.go | 13 ++++++++++--- docs/source/duo/ask.md | 14 ++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/commands/duo/ask/ask.go b/commands/duo/ask/ask.go index 0d79902d6..7af4f1ae7 100644 --- a/commands/duo/ask/ask.go +++ b/commands/duo/ask/ask.go @@ -7,6 +7,7 @@ import ( "os/exec" "regexp" "strings" + "os" "gitlab.com/gitlab-org/cli/commands/cmdutils" "gitlab.com/gitlab-org/cli/internal/run" @@ -152,12 +153,18 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { } if opts.Shell { + shell := os.Getenv("SHELL") + shellType := "bash" + if strings.Contains(shell, "zsh") { + shellType = "zsh" + } - opts.Prompt = "Convert this to the correct command: " + + opts.Prompt = "Convert this to the correct command for " + shellType + ": " + opts.Prompt + ". Give me only the exact command to run, nothing else. " + - "Don't be biased towards git commands - choose the best bash/shell tool for the job. " + - "Do not use dangerous system-modifying commands." + "Choose the best " + shellType + " tool for the job. " + + "Do not use dangerous system-modifying commands. " + + "Use " + shellType + "-specific features when they would improve the command." } result, err := opts.Result() diff --git a/docs/source/duo/ask.md b/docs/source/duo/ask.md index 296c0554e..1503a6ded 100644 --- a/docs/source/duo/ask.md +++ b/docs/source/duo/ask.md @@ -11,11 +11,13 @@ Please do not edit this file directly. Run `make gen-docs` instead. # `glab duo ask` -Generate Git commands from natural language. +Generate Git or shell commands from natural language ## Synopsis -Generate Git commands from natural language. +Generate Git or shell commands from natural language descriptions. + +Use --git (default) for Git-related commands or --shell for general shell commands. ```plaintext glab duo ask [flags] @@ -24,16 +26,20 @@ glab duo ask [flags] ## Examples ```plaintext +# Get Git commands with explanation $ glab duo ask list last 10 commit titles -# => A list of Git commands to show the titles of the latest 10 commits with an explanation and an option to execute the commands. +# Get a shell command +$ glab duo ask --shell list all pdf files + ``` ## Options ```plaintext - --git Ask a question about Git. (default true) + --git Ask a question about Git + --shell Generate shell commands from natural language ``` ## Options inherited from parent commands -- GitLab From 95d1b108c664b5baf32ba145137e16570f808c9f Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Mon, 25 Nov 2024 22:06:54 +1100 Subject: [PATCH 03/11] style: Apply gofumpt formatting to commands/duo/ask/ask.go --- commands/duo/ask/ask.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/duo/ask/ask.go b/commands/duo/ask/ask.go index 7af4f1ae7..8731dfa5f 100644 --- a/commands/duo/ask/ask.go +++ b/commands/duo/ask/ask.go @@ -4,10 +4,10 @@ import ( "errors" "fmt" "net/http" + "os" "os/exec" "regexp" "strings" - "os" "gitlab.com/gitlab-org/cli/commands/cmdutils" "gitlab.com/gitlab-org/cli/internal/run" @@ -159,7 +159,7 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { shellType = "zsh" } - opts.Prompt = "Convert this to the correct command for " + shellType + ": " + + opts.Prompt = "Convert this to the correct command for " + shellType + ": " + opts.Prompt + ". Give me only the exact command to run, nothing else. " + "Choose the best " + shellType + " tool for the job. " + -- GitLab From bddc948b71bd6e65fa21feb44bfe80956f88d812 Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Mon, 25 Nov 2024 22:14:14 +1100 Subject: [PATCH 04/11] style: Fix gofumpt formatting in ask.go and ask_test.go --- commands/duo/ask/ask_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/duo/ask/ask_test.go b/commands/duo/ask/ask_test.go index ab1d1cb60..640fe1a82 100644 --- a/commands/duo/ask/ask_test.go +++ b/commands/duo/ask/ask_test.go @@ -279,7 +279,7 @@ func TestInputValidation(t *testing.T) { }, { desc: "dangerous sudo command", - input: "--shell \"sudo apt-get update\"", + input: "--shell \"sudo apt-get update\"", expectedErr: "dangerous command pattern detected: sudo", }, { -- GitLab From b8fbac7344d5303d2e50f6c76ab65c2d274b051d Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Mon, 25 Nov 2024 22:15:55 +1100 Subject: [PATCH 05/11] fixing golangci-lint gofumpt linting errors --- commands/duo/ask/ask.go | 6 +++--- commands/duo/ask/ask_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/commands/duo/ask/ask.go b/commands/duo/ask/ask.go index 8731dfa5f..459ff1561 100644 --- a/commands/duo/ask/ask.go +++ b/commands/duo/ask/ask.go @@ -120,9 +120,9 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { ":(){ :|:& };:", "> /dev/sd", "mv /* /dev/null", - "wget", // Prevent arbitrary downloads - "curl", // Prevent arbitrary downloads - "sudo", // Prevent privilege escalation + "wget", // Prevent arbitrary downloads + "curl", // Prevent arbitrary downloads + "sudo", // Prevent privilege escalation } // Check both raw input and prompt for dangerous patterns diff --git a/commands/duo/ask/ask_test.go b/commands/duo/ask/ask_test.go index 640fe1a82..6094ca6d3 100644 --- a/commands/duo/ask/ask_test.go +++ b/commands/duo/ask/ask_test.go @@ -220,7 +220,7 @@ func TestFlagCombinations(t *testing.T) { }, { desc: "no flags provided", - args: "list files", + args: "list files", expectedOutput: "Commands:", // Just checking start of output since default is git mode }, } @@ -239,7 +239,7 @@ func TestFlagCombinations(t *testing.T) { } output, err := runCommand(fakeHTTP, false, tc.args) - + if tc.expectedErr != "" { require.Error(t, err) require.Contains(t, err.Error(), tc.expectedErr) -- GitLab From 0acf9998e12c12ddf8cb4a76b08c4d5e0475111d Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Wed, 27 Nov 2024 11:51:54 +1100 Subject: [PATCH 06/11] Moving git_command to chat completions endpoint --- commands/duo/ask/ask.go | 83 ++++++++++++++++++++++-------------- commands/duo/ask/ask_test.go | 24 +++++------ 2 files changed, 63 insertions(+), 44 deletions(-) diff --git a/commands/duo/ask/ask.go b/commands/duo/ask/ask.go index 459ff1561..25ee25457 100644 --- a/commands/duo/ask/ask.go +++ b/commands/duo/ask/ask.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "os" "os/exec" "regexp" "strings" @@ -25,7 +24,7 @@ type request struct { Model string `json:"model"` } -type response struct { +type gitResponse struct { Predictions []struct { Candidates []struct { Content string `json:"content"` @@ -33,6 +32,8 @@ type response struct { } `json:"predictions"` } +type chatResponse string + type result struct { Commands []string `json:"commands"` Explanation string `json:"explanation"` @@ -56,6 +57,7 @@ const ( runCmdsQuestion = "Would you like to run these Git commands?" gitCmd = "git" gitCmdAPIPath = "ai/llm/git_command" + chatAPIPath = "chat/completions" spinnerText = "Generating Git commands..." aiResponseErr = "Error: AI response has not been generated correctly." apiUnreachableErr = "Error: API is unreachable." @@ -153,13 +155,8 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { } if opts.Shell { - shell := os.Getenv("SHELL") - shellType := "bash" - if strings.Contains(shell, "zsh") { - shellType = "zsh" - } - - opts.Prompt = "Convert this to the correct command for " + shellType + ": " + + shellType := "shell" + opts.Prompt = "Convert this to a command: " + opts.Prompt + ". Give me only the exact command to run, nothing else. " + "Choose the best " + shellType + " tool for the job. " + @@ -179,19 +176,19 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { return errors.New(aiResponseErr) } // For shell mode, extract just the command without explanation - if cmd := cmdExecRegexp.FindString(content); cmd != "" { - // Remove the markdown code block markers - cmd = strings.TrimPrefix(cmd, "```") - cmd = strings.TrimSuffix(cmd, "```") - fmt.Fprint(opts.IO.StdOut, strings.TrimSpace(cmd)) - } else if cmd := cmdHighlightRegexp.FindString(content); cmd != "" { - // Try alternate code block style - cmd = strings.Trim(cmd, "`") - fmt.Fprint(opts.IO.StdOut, strings.TrimSpace(cmd)) - } else { - // If no code blocks found, use the raw content - fmt.Fprint(opts.IO.StdOut, strings.TrimSpace(content)) + // Extract and clean up command, removing any shell prefixes + cmd := content + if extracted := cmdExecRegexp.FindString(content); extracted != "" { + cmd = strings.Trim(extracted, "```") + } else if extracted := cmdHighlightRegexp.FindString(content); extracted != "" { + cmd = strings.Trim(extracted, "`") } + // Remove common shell prefixes and clean whitespace + cmd = strings.TrimSpace(cmd) + cmd = strings.TrimPrefix(cmd, "bash") + cmd = strings.TrimPrefix(cmd, "sh") + cmd = strings.TrimPrefix(cmd, "$") + fmt.Fprint(opts.IO.StdOut, cmd) // Changed from Fprintln to Fprint return nil } @@ -221,24 +218,46 @@ func (opts *opts) Result() (*result, error) { return nil, cmdutils.WrapError(err, "failed to get HTTP client.") } - body := request{Prompt: opts.Prompt, Model: vertexAI} - request, err := client.NewRequest(http.MethodPost, gitCmdAPIPath, body, nil) - if err != nil { - return nil, cmdutils.WrapError(err, "failed to create a request.") + apiPath := gitCmdAPIPath + if opts.Shell { + apiPath = chatAPIPath + } + var apiReq interface{} + if opts.Shell { + // For chat endpoint + apiReq = map[string]string{ + "content": opts.Prompt, + } + } else { + // For git command endpoint + apiReq = request{Prompt: opts.Prompt, Model: vertexAI} } - var r response - _, err = client.Do(request, &r) + req, err := client.NewRequest(http.MethodPost, apiPath, apiReq, nil) if err != nil { - return nil, cmdutils.WrapError(err, apiUnreachableErr) + return nil, cmdutils.WrapError(err, "failed to create a request.") } - if len(r.Predictions) == 0 || len(r.Predictions[0].Candidates) == 0 { - return nil, errors.New(aiResponseErr) + var content string + if opts.Shell { + var r chatResponse + _, err = client.Do(req, &r) + if err != nil { + return nil, cmdutils.WrapError(err, apiUnreachableErr) + } + content = string(r) + } else { + var r gitResponse + _, err = client.Do(req, &r) + if err != nil { + return nil, cmdutils.WrapError(err, apiUnreachableErr) + } + if len(r.Predictions) == 0 || len(r.Predictions[0].Candidates) == 0 { + return nil, errors.New(aiResponseErr) + } + content = r.Predictions[0].Candidates[0].Content } - content := r.Predictions[0].Candidates[0].Content - var cmds []string for _, cmd := range cmdExecRegexp.FindAllString(content, -1) { cmds = append(cmds, strings.Trim(cmd, "\n`")) diff --git a/commands/duo/ask/ask_test.go b/commands/duo/ask/ask_test.go index 6094ca6d3..ea46a2151 100644 --- a/commands/duo/ask/ask_test.go +++ b/commands/duo/ask/ask_test.go @@ -43,9 +43,9 @@ func runShellCommandTests(t *testing.T) { } defer fakeHTTP.Verify(t) - body := `{"predictions": [{ "candidates": [ {"content": "ls -la"} ]}]}` + body := `"ls -la"` response := httpmock.NewStringResponse(http.StatusOK, body) - fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/chat/completions", response) expectedOutput := "ls -la" output, err := runCommand(fakeHTTP, false, "--shell --git=false list files") @@ -59,9 +59,9 @@ func runShellCommandTests(t *testing.T) { } defer fakeHTTP.Verify(t) - body := `{"predictions": [{ "candidates": [ {"content": "find . -type f -name '*.txt' -mtime -7 | xargs grep 'pattern'"} ]}]}` + body := `"find . -type f -name '*.txt' -mtime -7 | xargs grep 'pattern'"` response := httpmock.NewStringResponse(http.StatusOK, body) - fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/chat/completions", response) expectedOutput := "find . -type f -name '*.txt' -mtime -7 | xargs grep 'pattern'" output, err := runCommand(fakeHTTP, false, "--shell --git=false find text files modified in last week containing pattern") @@ -75,9 +75,9 @@ func runShellCommandTests(t *testing.T) { } defer fakeHTTP.Verify(t) - body := `{"predictions": [{ "candidates": [ {"content": "echo \"Hello, World!\" > output.txt && sed -i 's/World/Everyone/g' output.txt"} ]}]}` + body := `"echo \"Hello, World!\" > output.txt && sed -i 's/World/Everyone/g' output.txt"` response := httpmock.NewStringResponse(http.StatusOK, body) - fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/chat/completions", response) expectedOutput := "echo \"Hello, World!\" > output.txt && sed -i 's/World/Everyone/g' output.txt" output, err := runCommand(fakeHTTP, false, "--shell --git=false create file saying Hello World and replace World with Everyone") @@ -91,9 +91,9 @@ func runShellCommandTests(t *testing.T) { } defer fakeHTTP.Verify(t) - body := `{"predictions": []}` + body := `""` response := httpmock.NewStringResponse(http.StatusOK, body) - fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/chat/completions", response) _, err := runCommand(fakeHTTP, false, "--shell --git=false list files") require.Error(t, err) @@ -106,9 +106,9 @@ func runShellCommandTests(t *testing.T) { } defer fakeHTTP.Verify(t) - body := `{"predictions": [{ "candidates": [{}]}]}` + body := `""` response := httpmock.NewStringResponse(http.StatusOK, body) - fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/chat/completions", response) _, err := runCommand(fakeHTTP, false, "--shell --git=false list files") require.Error(t, err) @@ -121,9 +121,9 @@ func runShellCommandTests(t *testing.T) { } defer fakeHTTP.Verify(t) - body := `{"predictions": [{ "candidates": [{"content": ""}]}]}` + body := `""` response := httpmock.NewStringResponse(http.StatusOK, body) - fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/ai/llm/git_command", response) + fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/chat/completions", response) _, err := runCommand(fakeHTTP, false, "--shell --git=false list files") require.Error(t, err) -- GitLab From c2f1b8f434cab4a61d38f97f0b07dd570736a806 Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Wed, 27 Nov 2024 12:15:18 +1100 Subject: [PATCH 07/11] feat: Suppress new release messages in shell assistant scripts --- scripts/shell-assistant/assistant.bash | 2 +- scripts/shell-assistant/assistant.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/shell-assistant/assistant.bash b/scripts/shell-assistant/assistant.bash index b82160650..8d5c2bbe7 100644 --- a/scripts/shell-assistant/assistant.bash +++ b/scripts/shell-assistant/assistant.bash @@ -2,7 +2,7 @@ _glab_duo_bash() { if [[ -n "$READLINE_LINE" ]]; then echo -en "\rGenerating command...\r" - local command=$(glab duo ask --shell "$READLINE_LINE" | head -n1 | tr -d '\r\n') + local command=$(glab duo ask --shell "$READLINE_LINE" 2>/dev/null | head -n1 | tr -d '\r\n') echo -en "\r \r" diff --git a/scripts/shell-assistant/assistant.zsh b/scripts/shell-assistant/assistant.zsh index d86c72969..086e6b92d 100644 --- a/scripts/shell-assistant/assistant.zsh +++ b/scripts/shell-assistant/assistant.zsh @@ -3,7 +3,7 @@ function _glab_duo_zsh() { echo -en "\rGenerating command...\r" # Get command suggestion using shell command - local command=$(glab duo ask --shell "$BUFFER" | head -n1 | tr -d '\r\n') + local command=$(glab duo ask --shell "$BUFFER" 2>/dev/null | head -n1 | tr -d '\r\n') # Clear the status message with spaces and reset cursor echo -en "\r \r" -- GitLab From d62d64bc32bb2ee54391f2715b53b37b4628cca3 Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Wed, 27 Nov 2024 12:19:50 +1100 Subject: [PATCH 08/11] Fixing golangci-lint errors --- commands/duo/ask/ask.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/duo/ask/ask.go b/commands/duo/ask/ask.go index 25ee25457..1332ba127 100644 --- a/commands/duo/ask/ask.go +++ b/commands/duo/ask/ask.go @@ -179,9 +179,9 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { // Extract and clean up command, removing any shell prefixes cmd := content if extracted := cmdExecRegexp.FindString(content); extracted != "" { - cmd = strings.Trim(extracted, "```") + cmd = strings.ReplaceAll(extracted, "```", "") } else if extracted := cmdHighlightRegexp.FindString(content); extracted != "" { - cmd = strings.Trim(extracted, "`") + cmd = strings.ReplaceAll(extracted, "`", "") } // Remove common shell prefixes and clean whitespace cmd = strings.TrimSpace(cmd) -- GitLab From 9fcd5a713d9ad1aa3b74ec88671b070eea27ab9c Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Wed, 27 Nov 2024 23:58:17 +0000 Subject: [PATCH 09/11] chore: Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Ezekiel Kigbo <3397881-ekigbo@users.noreply.gitlab.com> --- commands/duo/ask/ask.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/duo/ask/ask.go b/commands/duo/ask/ask.go index 1332ba127..1b5f6e2fc 100644 --- a/commands/duo/ask/ask.go +++ b/commands/duo/ask/ask.go @@ -81,7 +81,7 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { # Get Git commands with explanation $ glab duo ask list last 10 commit titles - # Get a shell command + # Get a shell command with explanation $ glab duo ask --shell list all pdf files `), -- GitLab From 0d71ffd8d9a9b3d44863d2eedd1c6244691148c5 Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Thu, 28 Nov 2024 17:19:44 +1100 Subject: [PATCH 10/11] Fixes for code review --- commands/duo/ask/ask.go | 199 ++++++++++++++++++++++------------------ 1 file changed, 109 insertions(+), 90 deletions(-) diff --git a/commands/duo/ask/ask.go b/commands/duo/ask/ask.go index 1b5f6e2fc..129001850 100644 --- a/commands/duo/ask/ask.go +++ b/commands/duo/ask/ask.go @@ -47,6 +47,64 @@ type opts struct { Shell bool } +func validateInput(args []string) error { + if len(args) == 0 { + return fmt.Errorf("prompt required") + } + + // Validate prompt length + if len(strings.Join(args, " ")) > 1000 { + return fmt.Errorf("prompt too long") + } + + // Check for dangerous characters + for _, arg := range args { + if strings.ContainsAny(arg, ";|&$") { + return fmt.Errorf("invalid characters in prompt") + } + } + + rawInput := strings.ToLower(strings.Join(args, " ")) + + // Define all dangerous patterns + dangerousPatterns := []string{ + "rm -rf /", + "rm -r /", + "mkfs", + "dd if=", + ":(){ :|:& };:", + "> /dev/sd", + "mv /* /dev/null", + "wget", // Prevent arbitrary downloads + "curl", // Prevent arbitrary downloads + "sudo", // Prevent privilege escalation + } + + // Check for dangerous patterns + for _, pattern := range dangerousPatterns { + if strings.Contains(rawInput, pattern) { + return fmt.Errorf("dangerous command pattern detected: %s", pattern) + } + } + + // Check for dangerous keywords + dangerousKeywords := []string{ + "remove all files", + "delete everything", + "format disk", + "wipe", + "destroy", + } + + for _, keyword := range dangerousKeywords { + if strings.Contains(rawInput, keyword) { + return fmt.Errorf("dangerous command pattern detected: rm -rf /") + } + } + + return nil +} + var ( cmdHighlightRegexp = regexp.MustCompile("`+\n?([^`]*)\n?`+\n?") cmdExecRegexp = regexp.MustCompile("```([^`]*)```") @@ -58,7 +116,8 @@ const ( gitCmd = "git" gitCmdAPIPath = "ai/llm/git_command" chatAPIPath = "chat/completions" - spinnerText = "Generating Git commands..." + gitSpinnerText = "Generating Git commands..." + shellSpinnerText = "Generating shell command..." aiResponseErr = "Error: AI response has not been generated correctly." apiUnreachableErr = "Error: API is unreachable." ) @@ -86,82 +145,25 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { `), RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return fmt.Errorf("prompt required") - } - - // Validate prompt length - if len(strings.Join(args, " ")) > 1000 { - return fmt.Errorf("prompt too long") - } - // Check for mutually exclusive flags first if opts.Git && opts.Shell { return fmt.Errorf("cannot use both --git and --shell flags") } - // Check for dangerous characters - for _, arg := range args { - if strings.ContainsAny(arg, ";|&$") { - return fmt.Errorf("invalid characters in prompt") - } - } - - opts.Prompt = strings.Join(args, " ") - - // Check for dangerous patterns - rawInput := strings.ToLower(strings.Join(args, " ")) - promptLower := strings.ToLower(opts.Prompt) - - // Define all dangerous patterns - dangerousPatterns := []string{ - "rm -rf /", - "rm -r /", - "mkfs", - "dd if=", - ":(){ :|:& };:", - "> /dev/sd", - "mv /* /dev/null", - "wget", // Prevent arbitrary downloads - "curl", // Prevent arbitrary downloads - "sudo", // Prevent privilege escalation - } - - // Check both raw input and prompt for dangerous patterns - for _, pattern := range dangerousPatterns { - if strings.Contains(rawInput, pattern) || strings.Contains(promptLower, pattern) { - return fmt.Errorf("dangerous command pattern detected: %s", pattern) - } - } - - // Check for dangerous keywords - dangerousKeywords := []string{ - "remove all files", - "delete everything", - "format disk", - "wipe", - "destroy", - } - - for _, keyword := range dangerousKeywords { - if strings.Contains(rawInput, keyword) || strings.Contains(promptLower, keyword) { - return fmt.Errorf("dangerous command pattern detected: rm -rf /") - } - } - // Default to Git mode if no flags set if !opts.Shell && !opts.Git { opts.Git = true } + // Validate input after flag checks + if err := validateInput(args); err != nil { + return err + } + + opts.Prompt = strings.Join(args, " ") + if opts.Shell { - shellType := "shell" - opts.Prompt = "Convert this to a command: " + - opts.Prompt + - ". Give me only the exact command to run, nothing else. " + - "Choose the best " + shellType + " tool for the job. " + - "Do not use dangerous system-modifying commands. " + - "Use " + shellType + "-specific features when they would improve the command." + return opts.executeShellCommand(args) } result, err := opts.Result() @@ -169,29 +171,6 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { return err } - if opts.Shell { - // For shell mode, print the raw command from the response - content := result.Explanation - if content == "" { - return errors.New(aiResponseErr) - } - // For shell mode, extract just the command without explanation - // Extract and clean up command, removing any shell prefixes - cmd := content - if extracted := cmdExecRegexp.FindString(content); extracted != "" { - cmd = strings.ReplaceAll(extracted, "```", "") - } else if extracted := cmdHighlightRegexp.FindString(content); extracted != "" { - cmd = strings.ReplaceAll(extracted, "`", "") - } - // Remove common shell prefixes and clean whitespace - cmd = strings.TrimSpace(cmd) - cmd = strings.TrimPrefix(cmd, "bash") - cmd = strings.TrimPrefix(cmd, "sh") - cmd = strings.TrimPrefix(cmd, "$") - fmt.Fprint(opts.IO.StdOut, cmd) // Changed from Fprintln to Fprint - return nil - } - opts.displayResult(result) if len(result.Commands) > 0 { @@ -210,7 +189,11 @@ func NewCmdAsk(f *cmdutils.Factory) *cobra.Command { } func (opts *opts) Result() (*result, error) { - opts.IO.StartSpinner(spinnerText) + spinnerMsg := gitSpinnerText + if opts.Shell { + spinnerMsg = shellSpinnerText + } + opts.IO.StartSpinner(spinnerMsg) defer opts.IO.StopSpinner("") client, err := opts.HttpClient() @@ -310,6 +293,42 @@ func (opts *opts) executeCommands(commands []string) error { return nil } +func (opts *opts) executeShellCommand(args []string) error { + shellType := "shell" + opts.Prompt = "Convert this to a command: " + + strings.Join(args, " ") + + ". Give me only the exact command to run, nothing else. " + + "Choose the best " + shellType + " tool for the job. " + + "Do not use dangerous system-modifying commands. " + + "Use " + shellType + "-specific features when they would improve the command." + + result, err := opts.Result() + if err != nil { + return err + } + content := result.Explanation + if content == "" { + return errors.New(aiResponseErr) + } + + // Extract and clean up command, removing any shell prefixes + cmd := content + if extracted := cmdExecRegexp.FindString(content); extracted != "" { + cmd = strings.ReplaceAll(extracted, "```", "") + } else if extracted := cmdHighlightRegexp.FindString(content); extracted != "" { + cmd = strings.ReplaceAll(extracted, "`", "") + } + + // Remove common shell prefixes and clean whitespace + cmd = strings.TrimSpace(cmd) + cmd = strings.TrimPrefix(cmd, "bash") + cmd = strings.TrimPrefix(cmd, "sh") + cmd = strings.TrimPrefix(cmd, "$") + + fmt.Fprint(opts.IO.StdOut, cmd) + return nil +} + func (opts *opts) executeCommand(cmd string) error { gitArgs, err := shlex.Split(cmd) if err != nil { -- GitLab From 05efbe457812da81fbb75b4e68292e77a3e5917d Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Thu, 28 Nov 2024 18:39:20 +1100 Subject: [PATCH 11/11] Running make gen-docs --- docs/source/duo/ask.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/duo/ask.md b/docs/source/duo/ask.md index 1503a6ded..730e0e7df 100644 --- a/docs/source/duo/ask.md +++ b/docs/source/duo/ask.md @@ -29,7 +29,7 @@ glab duo ask [flags] # Get Git commands with explanation $ glab duo ask list last 10 commit titles -# Get a shell command +# Get a shell command with explanation $ glab duo ask --shell list all pdf files -- GitLab