Skip to content

Commit

Permalink
feat: add editor flag to generate (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
gphorvath authored Nov 12, 2024
1 parent d4578ce commit 6eb0f02
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 84 deletions.
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

0 comments on commit 6eb0f02

Please sign in to comment.