Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add editor flag to generate #16

Merged
merged 4 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .lycheeignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
github\.com/private-org/.*
file.*/issues
file.*/issues
localhost:\d+/.*
http://localhost:\d+/.*
https://localhost:\d+/.*
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ A prompt engineer's ***Grimoire*** to assist with getting the best results from
To install Grimoire, follow these steps:

1. Clone the repository and navigate to the directory:

```sh
git clone https://github.com/gphorvath/grimoire.git
cd grimoire
```

2. Build and install using Make:
1. Build and install using Make:

```sh
# Build the binary
make build
Expand All @@ -31,6 +33,7 @@ make install
```

This will:

- Build the Grimoire binary
- Create the ~/.grimoire directory
- Copy default prompts to ~/.grimoire/prompts
Expand All @@ -50,14 +53,37 @@ grimoire copy <prompt-name>
# Create a new prompt
grimoire new <prompt-name>

# Edit an existing prompt
# Edit an existing prompt in default editor
grimoire edit <prompt-name>

# Use echo prompt to verify prompt loading
# Requires Ollama running locally with a configured model (default: llama3)
grimoire generate -p echo "Testing generation functionality"
grimoire generate --prompt echo "Testing generation functionality"

# Modify prompt in default editor before generation (doesn't require arg)
grimoire generate --prompt echo --edit
```

## Environment Variables

GRIMOIRE environment variables control the configuration of the application.

### Ollama Configuration

- `GRIMOIRE_OLLAMA_MODEL` (default: "llama3")
The model to be used with Ollama for AI inference.

- `GRIMOIRE_OLLAMA_URL` (default: "<http://localhost:11434/api>")
The URL endpoint for the Ollama API server.

- `GRIMOIRE_OLLAMA_STREAM` (default: true)
Controls whether to stream responses from Ollama. Set to "true" to enable streaming, "false" to disable.

### Editor Configuration

- `GRIMOIRE_EDITOR` (default: "vim")
The text editor to use when editing prompts or configuration files.

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
68 changes: 68 additions & 0 deletions src/cmd/common/file_operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package common

import (
"errors"
"os"
"os/exec"
"path/filepath"

"github.com/gphorvath/grimoire/src/config"
)

// FileExists reports if a file exists
func FileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}

// CreateIfNotExists creates a file if it does not exist
// and returns an error if it fails to create the file
func CreateIfNotExists(filePath string) error {
if !FileExists(filePath) {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
}
return nil
}

// FindAndJoin walks the directory tree rooted at baseDir
// and returns the path of the first file with the given filename
// and an error if the file is not found
func FindAndJoin(baseDir, filename string) (string, error) {
var foundPath string
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() == filename {
foundPath = path
return filepath.SkipDir
}
return nil
})
if err != nil {
return "", err
}
if foundPath == "" {
return "", os.ErrNotExist
}
return foundPath, nil
}

// OpenInEditor opens a file in the configured editor
// and returns an error if it fails to open the file
func OpenInEditor(path string) error {
if !FileExists(path) {
return errors.New("file does not exist")
}

cmd := exec.Command(config.Editor, path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin

return cmd.Run()
}
12 changes: 2 additions & 10 deletions src/cmd/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package cmd
import (
"fmt"
"os"
"path/filepath"

"github.com/gphorvath/grimoire/src/cmd/common"
"github.com/gphorvath/grimoire/src/config"
"github.com/spf13/cobra"
"golang.design/x/clipboard"
Expand All @@ -24,20 +24,12 @@ func init() {
func runCopyCmd(cmd *cobra.Command, args []string) {
baseDir := config.GetPromptDir()
filename := args[0] + ".md"

dir, err := findFileDir(baseDir, filename)
filePath, err := common.FindAndJoin(baseDir, filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if dir == "" {
fmt.Fprintf(os.Stderr, "Error: prompt not found\n")
os.Exit(1)
}

filePath := filepath.Join(dir, filename)

// Init returns an error if the package is not ready for use.
err = clipboard.Init()
if err != nil {
Expand Down
10 changes: 2 additions & 8 deletions src/cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package cmd
import (
"fmt"
"os"
"path/filepath"

"github.com/gphorvath/grimoire/src/cmd/common"
"github.com/gphorvath/grimoire/src/config"
"github.com/spf13/cobra"
)
Expand All @@ -24,18 +24,12 @@ func runDeleteCmd(cmd *cobra.Command, args []string) {
baseDir := config.GetPromptDir()
filename := args[0] + ".md"

dir, err := findFileDir(baseDir, filename)
filePath, err := common.FindAndJoin(baseDir, filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if dir == "" {
fmt.Fprintf(os.Stderr, "Error: prompt not found\n")
os.Exit(1)
}

filePath := filepath.Join(dir, filename)
if err := os.Remove(filePath); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
Expand Down
60 changes: 14 additions & 46 deletions src/cmd/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package cmd
import (
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/gphorvath/grimoire/src/cmd/common"
"github.com/gphorvath/grimoire/src/config"
"github.com/spf13/cobra"
)
Expand All @@ -25,62 +25,30 @@ func runEditCmd(cmd *cobra.Command, args []string) {
baseDir := config.GetPromptDir()
filename := args[0] + ".md"

dir, err := findFileDir(baseDir, filename)
if err != nil {
// Try to find existing file
filePath, err := common.FindAndJoin(baseDir, filename)
if err != nil && !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if dir == "" {
dir = baseDir
filePath := filepath.Join(dir, filename)
if err := createNewFile(filePath); err != nil {
// If file doesn't exist, create it in the base directory
if filePath == "" {
filePath = filepath.Join(baseDir, filename)
if err := common.CreateIfNotExists(filePath); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Write example prompt to the new file
if err := os.WriteFile(filePath, []byte(config.ExamplePrompt), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Created new prompt file %s\n", filePath)
}

filePath := filepath.Join(dir, filename)
if err := openEditor(filePath); err != nil {
if err := common.OpenInEditor(filePath); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func findFileDir(baseDir, filename string) (string, error) {
var dir string
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() == filename {
dir = filepath.Dir(path)
return filepath.SkipDir
}
return nil
})
return dir, err
}

func createNewFile(filePath string) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()

_, err = file.WriteString(config.ExamplePrompt)
return err
}

func openEditor(filePath string) error {
editor := config.Editor

editCmd := exec.Command(editor, filePath)
editCmd.Stdin = os.Stdin
editCmd.Stdout = os.Stdout
editCmd.Stderr = os.Stderr

return editCmd.Run()
}
53 changes: 41 additions & 12 deletions src/cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/gphorvath/grimoire/src/cmd/common"
"github.com/gphorvath/grimoire/src/config"
"github.com/spf13/cobra"
)
Expand All @@ -25,22 +25,29 @@ type OllamaResponse struct {
}

var (
promptFile string
promptFile string
editBeforeGenerate bool

generateCmd = &cobra.Command{
Use: "generate [flags] [input...]",
Short: "Get generation from Ollama",
Long: "Requests generation from Ollama while optionally prepending a prompt",
Args: cobra.MinimumNArgs(1),
RunE: runGenerate,
Args: func(cmd *cobra.Command, args []string) error {
editFlag, _ := cmd.Flags().GetBool("edit")
if !editFlag && len(args) < 1 {
return fmt.Errorf("requires at least 1 arg when not using edit flag")
}
return nil
},
RunE: runGenerate,
}
)

func init() {
rootCmd.AddCommand(generateCmd)
generateCmd.Flags().StringVarP(&promptFile, "prompt", "p", "", "Prompt file to prepend to input")
generateCmd.Flags().BoolVarP(&editBeforeGenerate, "edit", "e", false, "Edit input before generating")
}

func runGenerate(cmd *cobra.Command, args []string) error {
// Join all arguments as the input text
input := strings.Join(args, " ")
Expand All @@ -51,17 +58,12 @@ func runGenerate(cmd *cobra.Command, args []string) error {
baseDir := config.GetPromptDir()
filename := promptFile + ".md"

dir, err := findFileDir(baseDir, filename)
filepath, err := common.FindAndJoin(baseDir, filename)
if err != nil {
return err
}

if dir == "" {
return fmt.Errorf("prompt not found")
}

filePath := filepath.Join(dir, filename)
content, err := os.ReadFile(filePath)
content, err := os.ReadFile(filepath)
if err != nil {
return err
}
Expand All @@ -71,6 +73,33 @@ func runGenerate(cmd *cobra.Command, args []string) error {
finalPrompt = input
}

if editBeforeGenerate {
// Create temporary file for editing
tmpFile, err := os.CreateTemp("", "grimoire-*.txt")
if err != nil {
return err
}
defer os.Remove(tmpFile.Name())

// Write prompt to temp file
if _, err := tmpFile.WriteString(finalPrompt); err != nil {
return err
}
tmpFile.Close()

// Open in editor
if err := common.OpenInEditor(tmpFile.Name()); err != nil {
return err
}

// Read back edited content
content, err := os.ReadFile(tmpFile.Name())
if err != nil {
return err
}
finalPrompt = string(content)
}

// Create request body
reqBody := OllamaRequest{
Model: config.OllamaModel,
Expand Down
Loading