diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8e3a8ad..c5d8261 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -61,12 +61,4 @@ jobs: with: go-version: '1.24' - name: Build the code - run: go build -o gromitai . - - name: Run cli with no commands and check the usage output - run: | - output=$(./gromitai || true) - echo "$output" - if [[ "$output" != *"⚡️🤖 Please run ./gromit --help to see usage"* ]]; then - echo "❌ Usage message does not match the expected value" - exit 1 - fi \ No newline at end of file + run: go build -o gromitai . \ No newline at end of file diff --git a/gromit.go b/gromit.go index f14a326..6bd426b 100644 --- a/gromit.go +++ b/gromit.go @@ -8,6 +8,7 @@ import ( "io" "os" "os/exec" + "regexp" "runtime" "sort" "strings" @@ -18,8 +19,9 @@ import ( const systemPrompt = `You are an assistant providing terminal commands based on user's questions. You will be given a question about how to do something in the CLI environment. You will then find out what command to execute and provide the command. - Do not provide any additional information, explanation or context, just the linux command. - For example, if question is about listing all files in a directory for linux, respond with "ls".` + Make sure to enclose the actual command inside *** marker. + For example, if question is about listing all files in a directory for linux, respond with "***ls***". + If no question is asked by user, continue the conversation. If they want to exit, respond with "***exit***".` type Gromit struct { cli.Command @@ -131,10 +133,6 @@ func WithAskForConfirmation(confirm bool) ConfigurationModifier { func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { commandArgs := command.Args().Slice() query := strings.Join(commandArgs, " ") - if query == "" { - g.print("Please run ./gromit --help to see usage") - return nil - } prompt := g.String("systemPrompt") if prompt == "" { prompt = systemPrompt @@ -147,51 +145,74 @@ func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { model: g.String("model"), systemPrompt: prompt, } - err := g.handleUserQuery(ctx, query) + terminalCommand, err := g.extractCommandForQuery(ctx, query) if err != nil { return err } + if terminalCommand != "" { + err = g.handleTerminalCommand(ctx, terminalCommand) + if err != nil { + return err + } + } for ctx.Err() == nil { - confirmation, err := g.askConfirmation("Can I help you with anything else?") + //read the user input, pass it to AI + reader := bufio.NewReader(g.Reader) + query, err := reader.ReadString('\n') if err != nil { return err } - if confirmation.confirmed { - g.print("How can I help?") - reader := bufio.NewReader(os.Stdin) - query, err := reader.ReadString('\n') + terminalCommand, err := g.extractCommandForQuery(ctx, query) + if err != nil { + return err + } + if terminalCommand == "exit" { + break + } + if terminalCommand != "" { + err = g.handleTerminalCommand(ctx, terminalCommand) if err != nil { return err } - if err = g.handleUserQuery(ctx, query); err != nil { - return err - } - } else { - break } } return nil } -func (g *Gromit) handleUserQuery(ctx context.Context, query string) error { +func (g *Gromit) extractCommandForQuery(ctx context.Context, query string) (string, error) { assister, err := g.AssisterCreator.GetAssister(g.configuration.AiParameters) if err != nil { - return err + return "", err } - exeCommand, err := assister.GetTerminalCommand(ctx, query) + if query == "" { + query = "Can you please introduce yourself or continue the conversation?" + } + response, err := assister.GetTerminalCommand(ctx, query) if err != nil { - return err + return "", err } - g.print("In order to do that, you need to run:") - g.print(exeCommand) + //command is enclosed in *** marker + regexp := regexp.MustCompile(`\*\*\*(.*?)\*\*\*`) + commands := regexp.FindStringSubmatch(response) + var command string + if len(commands) > 0 { + command = commands[1] + } else { + g.print(response) + } + return command, nil +} +func (g *Gromit) handleTerminalCommand(ctx context.Context, terminalCommand string) error { + g.print("In order to do that, you need to run:") + g.print(terminalCommand) confirmation, err := g.askConfirmation("Would you like to run this command?") if err != nil { g.print("Error reading your response") return err } if confirmation.confirmed { - err = g.executeCommand(exeCommand) + err = g.executeCommand(terminalCommand) if err != nil { return err } diff --git a/gromit_test.go b/gromit_test.go index d8f7a8c..b2d9934 100644 --- a/gromit_test.go +++ b/gromit_test.go @@ -54,21 +54,13 @@ func TestMessagePrinter(t *testing.T) { require.Equal(t, "✌️ hello \r\n", buff.String()) } -func TestConfigurationPromptPrefix(t *testing.T) { - var buff bytes.Buffer - g, err := NewGromit(&mockAIProvider{}, WithPromptPrefix("🏝️"), WithWriter(&buff)) - require.NoError(t, err) - g.Run(t.Context(), []string{}) - require.Contains(t, buff.String(), "🏝️ Please run ./gromit --help to see usage") -} - func TestWhenAIProviderFailsToCreateAssister(t *testing.T) { m := &mockAIProvider{ assisterError: errors.New("Unable to create assister"), } g, err := NewGromit(m) require.NoError(t, err) - err = g.handleUserQuery(t.Context(), "some query") + _, err = g.extractCommandForQuery(t.Context(), "some query") require.EqualError(t, err, "Unable to create assister") } @@ -85,19 +77,18 @@ func TestWhenAIProviderFailsToFindTheCommand(t *testing.T) { func TestAIAssisterFindingCorrectCommand(t *testing.T) { var buff bytes.Buffer m := &mockAIProvider{ - commandResult: "ls", + commandResult: "***ls***", } g, err := NewGromit(m, WithWriter(&buff), WithPromptPrefix("🐶"), WithAskForConfirmation(false)) require.NoError(t, err) - + g.Reader = strings.NewReader("I want to list all files in current directory\n") g.Run(t.Context(), []string{"gromit", "--model", "myModel", "--agent", "myAgent", "--apiKey=key1234", "--maxTokens=2000", - "--systemPrompt", "myPrompt", "I", "want", "to", "list", "all", "files", "in", "current", "directory"}) + "--systemPrompt", "myPrompt", "hello", "my", "ai", "friend!"}) result := buff.String() require.Contains(t, result, "🐶 In order to do that, you need to run") require.Contains(t, result, "🐶 ls") require.Contains(t, result, "README.md") - require.Contains(t, result, "🐶 How can I help?") require.Equal(t, "myAgent", m.actualAiParameters.agent) require.Equal(t, "myModel", m.actualAiParameters.model) @@ -107,7 +98,7 @@ func TestAIAssisterFindingCorrectCommand(t *testing.T) { require.Contains(t, m.actualAiParameters.systemPrompt, "User's operating system is") require.Contains(t, m.actualAiParameters.systemPrompt, "User's current shell is") require.Contains(t, m.actualAiParameters.systemPrompt, "User's available path commands are") - require.Equal(t, "I want to list all files in current directory", m.actualUserMessage) + require.Contains(t, m.actualUserMessage, "I want to list all files in current directory") } type mockAIProvider struct {