summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/mcp/server.go87
-rw-r--r--internal/promptstore/builtin.go156
-rw-r--r--internal/promptstore/store.go53
-rw-r--r--internal/promptstore/store_test.go217
4 files changed, 162 insertions, 351 deletions
diff --git a/internal/mcp/server.go b/internal/mcp/server.go
index f6479d9..4bc66dd 100644
--- a/internal/mcp/server.go
+++ b/internal/mcp/server.go
@@ -318,20 +318,45 @@ func (s *Server) handlePromptsCreate(req Request) {
}
// Validate required fields
- if params.Name == "" {
- s.sendError(req.ID, ErrCodeInvalidParams, "Prompt name is required")
+ if err := validateCreateParams(params); err != nil {
+ s.sendError(req.ID, ErrCodeInvalidParams, err.Error())
return
}
- if params.Title == "" {
- s.sendError(req.ID, ErrCodeInvalidParams, "Prompt title is required")
+
+ // Build prompt from params
+ prompt := buildPromptFromCreateParams(params)
+
+ if err := s.store.Create(prompt); err != nil {
+ s.logger.Printf("create prompt error: %v", err)
+ s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to create prompt: %v", err))
return
}
+
+ result := PromptOperationResult{
+ Success: true,
+ Message: fmt.Sprintf("Created prompt: %s", params.Name),
+ }
+
+ s.logger.Printf("created prompt: %s", params.Name)
+ s.sendResponse(req.ID, result)
+}
+
+// validateCreateParams validates required fields for prompt creation.
+func validateCreateParams(params CreatePromptRequest) error {
+ if params.Name == "" {
+ return fmt.Errorf("prompt name is required")
+ }
+ if params.Title == "" {
+ return fmt.Errorf("prompt title is required")
+ }
if len(params.Messages) == 0 {
- s.sendError(req.ID, ErrCodeInvalidParams, "At least one message is required")
- return
+ return fmt.Errorf("at least one message is required")
}
+ return nil
+}
- // Create prompt
+// buildPromptFromCreateParams converts CreatePromptRequest to Prompt.
+func buildPromptFromCreateParams(params CreatePromptRequest) *promptstore.Prompt {
prompt := &promptstore.Prompt{
Name: params.Name,
Title: params.Title,
@@ -361,19 +386,7 @@ func (s *Server) handlePromptsCreate(req Request) {
})
}
- if err := s.store.Create(prompt); err != nil {
- s.logger.Printf("create prompt error: %v", err)
- s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to create prompt: %v", err))
- return
- }
-
- result := PromptOperationResult{
- Success: true,
- Message: fmt.Sprintf("Created prompt: %s", params.Name),
- }
-
- s.logger.Printf("created prompt: %s", params.Name)
- s.sendResponse(req.ID, result)
+ return prompt
}
// handlePromptsUpdate processes the prompts/update request.
@@ -406,6 +419,26 @@ func (s *Server) handlePromptsUpdate(req Request) {
return
}
+ // Apply updates to existing prompt
+ applyPromptUpdates(existing, params)
+
+ if err := s.store.Update(existing); err != nil {
+ s.logger.Printf("update prompt error: %v", err)
+ s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to update prompt: %v", err))
+ return
+ }
+
+ result := PromptOperationResult{
+ Success: true,
+ Message: fmt.Sprintf("Updated prompt: %s", params.Name),
+ }
+
+ s.logger.Printf("updated prompt: %s", params.Name)
+ s.sendResponse(req.ID, result)
+}
+
+// applyPromptUpdates applies update parameters to an existing prompt.
+func applyPromptUpdates(existing *promptstore.Prompt, params UpdatePromptRequest) {
// Update fields (only if provided)
if params.Title != "" {
existing.Title = params.Title
@@ -440,20 +473,6 @@ func (s *Server) handlePromptsUpdate(req Request) {
}
existing.Updated = time.Now()
-
- if err := s.store.Update(existing); err != nil {
- s.logger.Printf("update prompt error: %v", err)
- s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to update prompt: %v", err))
- return
- }
-
- result := PromptOperationResult{
- Success: true,
- Message: fmt.Sprintf("Updated prompt: %s", params.Name),
- }
-
- s.logger.Printf("updated prompt: %s", params.Name)
- s.sendResponse(req.ID, result)
}
// handlePromptsDelete processes the prompts/delete request.
diff --git a/internal/promptstore/builtin.go b/internal/promptstore/builtin.go
deleted file mode 100644
index ac4c830..0000000
--- a/internal/promptstore/builtin.go
+++ /dev/null
@@ -1,156 +0,0 @@
-// Summary: Built-in prompts for common development tasks.
-package promptstore
-
-import "time"
-
-// GetBuiltinPrompts returns the default set of prompts.
-// These are written to default.jsonl on first run.
-func GetBuiltinPrompts() []Prompt {
- now := time.Now()
-
- return []Prompt{
- {
- Name: "code_review",
- Title: "Request Code Review",
- Description: "Analyzes code quality, style, and suggests improvements",
- Arguments: []PromptArgument{
- {Name: "code", Description: "The code to review", Required: true},
- },
- Messages: []PromptMessage{
- {
- Role: "user",
- Content: MessageContent{
- Type: "text",
- Text: "Please review the following code for quality, style, and potential issues:\n\n{{code}}",
- },
- },
- },
- Tags: []string{"development", "review", "quality"},
- Created: now,
- Updated: now,
- },
- {
- Name: "explain_code",
- Title: "Explain Code",
- Description: "Provides detailed explanation of what code does",
- Arguments: []PromptArgument{
- {Name: "code", Description: "The code to explain", Required: true},
- },
- Messages: []PromptMessage{
- {
- Role: "user",
- Content: MessageContent{
- Type: "text",
- Text: "Please explain in detail what the following code does:\n\n{{code}}",
- },
- },
- },
- Tags: []string{"development", "documentation", "learning"},
- Created: now,
- Updated: now,
- },
- {
- Name: "generate_tests",
- Title: "Generate Unit Tests",
- Description: "Generates unit tests for a function or class",
- Arguments: []PromptArgument{
- {Name: "code", Description: "The code to test", Required: true},
- {Name: "language", Description: "Programming language", Required: false},
- },
- Messages: []PromptMessage{
- {
- Role: "user",
- Content: MessageContent{
- Type: "text",
- Text: "Generate comprehensive unit tests for the following code:\n\nLanguage: {{language}}\n\n{{code}}",
- },
- },
- },
- Tags: []string{"development", "testing", "tdd"},
- Created: now,
- Updated: now,
- },
- {
- Name: "document_function",
- Title: "Generate Documentation",
- Description: "Generates documentation comments and docstrings",
- Arguments: []PromptArgument{
- {Name: "code", Description: "The code to document", Required: true},
- },
- Messages: []PromptMessage{
- {
- Role: "user",
- Content: MessageContent{
- Type: "text",
- Text: "Generate comprehensive documentation for the following code:\n\n{{code}}",
- },
- },
- },
- Tags: []string{"development", "documentation"},
- Created: now,
- Updated: now,
- },
- {
- Name: "simplify_code",
- Title: "Simplify Code",
- Description: "Simplifies complex code while preserving behavior",
- Arguments: []PromptArgument{
- {Name: "code", Description: "The code to simplify", Required: true},
- },
- Messages: []PromptMessage{
- {
- Role: "user",
- Content: MessageContent{
- Type: "text",
- Text: "Simplify the following code while preserving its behavior:\n\n{{code}}",
- },
- },
- },
- Tags: []string{"development", "refactoring", "quality"},
- Created: now,
- Updated: now,
- },
- {
- Name: "fix_bugs",
- Title: "Analyze and Fix Bugs",
- Description: "Analyzes code for bugs and suggests fixes",
- Arguments: []PromptArgument{
- {Name: "code", Description: "The code to analyze", Required: true},
- {Name: "error", Description: "Error message or symptoms", Required: false},
- },
- Messages: []PromptMessage{
- {
- Role: "user",
- Content: MessageContent{
- Type: "text",
- Text: "Analyze the following code for bugs and suggest fixes:\n\nError: {{error}}\n\nCode:\n{{code}}",
- },
- },
- },
- Tags: []string{"development", "debugging", "bug-fix"},
- Created: now,
- Updated: now,
- },
- {
- Name: "refactor_extract",
- Title: "Extract Function",
- Description: "Extracts code into a separate, reusable function",
- Arguments: []PromptArgument{
- {Name: "code", Description: "The code to extract", Required: true},
- {Name: "function_name", Description: "Desired function name", Required: false},
- },
- Messages: []PromptMessage{
- {
- Role: "user",
- Content: MessageContent{
- Type: "text",
- Text: "Extract the following code into a separate function named {{function_name}}:\n\n{{code}}",
- },
- },
- },
- Tags: []string{"development", "refactoring"},
- Created: now,
- Updated: now,
- },
- }
-}
diff --git a/internal/promptstore/store.go b/internal/promptstore/store.go
index c1fcb9f..e789dda 100644
--- a/internal/promptstore/store.go
+++ b/internal/promptstore/store.go
@@ -72,39 +72,9 @@ func NewJSONLStore(dataDir string) (PromptStore, error) {
maxBackups: 10, // Keep last 10 backups
}
- // Initialize default.jsonl with built-in prompts if it doesn't exist
- defaultPath := filepath.Join(dataDir, "default.jsonl")
- if _, err := os.Stat(defaultPath); os.IsNotExist(err) {
- if err := store.writeBuiltinPrompts(); err != nil {
- return nil, fmt.Errorf("cannot write built-in prompts: %w", err)
- }
- }
-
return store, nil
}
-// writeBuiltinPrompts writes the built-in prompts to default.jsonl.
-func (s *JSONLStore) writeBuiltinPrompts() error {
- prompts := GetBuiltinPrompts()
- defaultPath := filepath.Join(s.dataDir, "default.jsonl")
-
- var lines []byte
- for _, p := range prompts {
- data, err := json.Marshal(p)
- if err != nil {
- return fmt.Errorf("marshal built-in prompt: %w", err)
- }
- lines = append(lines, data...)
- lines = append(lines, '\n')
- }
-
- if err := s.writeFileFn(defaultPath, lines, 0o644); err != nil {
- return fmt.Errorf("write default.jsonl: %w", err)
- }
-
- return nil
-}
-
// List returns prompts with pagination.
// cursor format: "<file>:<offset>" where file is "default" or "user", offset is line number.
func (s *JSONLStore) List(cursor string, limit int) ([]Prompt, string, error) {
@@ -321,33 +291,14 @@ func (s *JSONLStore) hasAllTags(promptTags, searchTags []string) bool {
return true
}
-// loadAllPrompts loads prompts from both default.jsonl and user.jsonl.
+// loadAllPrompts loads prompts from user.jsonl.
func (s *JSONLStore) loadAllPrompts() ([]Prompt, error) {
- defaultPrompts, err := s.loadPromptsFromFile("default.jsonl")
- if err != nil && !os.IsNotExist(err) {
- return nil, err
- }
-
userPrompts, err := s.loadPromptsFromFile("user.jsonl")
if err != nil && !os.IsNotExist(err) {
return nil, err
}
- // Merge (user prompts override built-ins by name)
- promptMap := make(map[string]Prompt)
- for _, p := range defaultPrompts {
- promptMap[p.Name] = p
- }
- for _, p := range userPrompts {
- promptMap[p.Name] = p
- }
-
- var all []Prompt
- for _, p := range promptMap {
- all = append(all, p)
- }
-
- return all, nil
+ return userPrompts, nil
}
// loadPromptsFromFile reads prompts from a JSONL file.
diff --git a/internal/promptstore/store_test.go b/internal/promptstore/store_test.go
index 8dd506a..167dcf6 100644
--- a/internal/promptstore/store_test.go
+++ b/internal/promptstore/store_test.go
@@ -2,60 +2,57 @@
package promptstore
import (
- "os"
- "path/filepath"
+ "fmt"
"testing"
"time"
)
func TestJSONLStore_Get(t *testing.T) {
- tests := []struct {
- name string
- promptName string
- want *Prompt
- wantErr bool
- }{
- {
- name: "get built-in prompt",
- promptName: "code_review",
- want: &Prompt{
- Name: "code_review",
- Title: "Request Code Review",
- Description: "Analyzes code quality, style, and suggests improvements",
- },
- wantErr: false,
- },
- {
- name: "prompt not found",
- promptName: "nonexistent",
- want: nil,
- wantErr: true,
- },
- }
+ t.Run("get existing prompt", func(t *testing.T) {
+ tmpDir := t.TempDir()
+ store, err := NewJSONLStore(tmpDir)
+ if err != nil {
+ t.Fatalf("NewJSONLStore() error = %v", err)
+ }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- tmpDir := t.TempDir()
- store, err := NewJSONLStore(tmpDir)
- if err != nil {
- t.Fatalf("NewJSONLStore() error = %v", err)
- }
-
- got, err := store.Get(tt.promptName)
- if (err != nil) != tt.wantErr {
- t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !tt.wantErr {
- if got.Name != tt.want.Name {
- t.Errorf("Get() name = %v, want %v", got.Name, tt.want.Name)
- }
- if got.Title != tt.want.Title {
- t.Errorf("Get() title = %v, want %v", got.Title, tt.want.Title)
- }
- }
- })
- }
+ // Create a test prompt first
+ testPrompt := &Prompt{
+ Name: "test_prompt",
+ Title: "Test Prompt",
+ Description: "A test prompt",
+ Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test"}}},
+ Created: time.Now(),
+ Updated: time.Now(),
+ }
+ if err := store.Create(testPrompt); err != nil {
+ t.Fatalf("Create() error = %v", err)
+ }
+
+ // Now get it
+ got, err := store.Get("test_prompt")
+ if err != nil {
+ t.Fatalf("Get() error = %v", err)
+ }
+ if got.Name != "test_prompt" {
+ t.Errorf("Get() name = %v, want test_prompt", got.Name)
+ }
+ if got.Title != "Test Prompt" {
+ t.Errorf("Get() title = %v, want Test Prompt", got.Title)
+ }
+ })
+
+ t.Run("prompt not found", func(t *testing.T) {
+ tmpDir := t.TempDir()
+ store, err := NewJSONLStore(tmpDir)
+ if err != nil {
+ t.Fatalf("NewJSONLStore() error = %v", err)
+ }
+
+ _, err = store.Get("nonexistent")
+ if err == nil {
+ t.Error("Get() expected error for nonexistent prompt, got nil")
+ }
+ })
}
func TestJSONLStore_List(t *testing.T) {
@@ -65,15 +62,29 @@ func TestJSONLStore_List(t *testing.T) {
t.Fatalf("NewJSONLStore() error = %v", err)
}
+ // Create test prompts
+ for i := 0; i < 7; i++ {
+ prompt := &Prompt{
+ Name: fmt.Sprintf("test%d", i),
+ Title: fmt.Sprintf("Test %d", i),
+ Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test"}}},
+ Created: time.Now(),
+ Updated: time.Now(),
+ }
+ if err := store.Create(prompt); err != nil {
+ t.Fatalf("Create() error = %v", err)
+ }
+ }
+
// List all prompts
prompts, cursor, err := store.List("", 100)
if err != nil {
t.Fatalf("List() error = %v", err)
}
- // Should have built-in prompts
- if len(prompts) < 5 {
- t.Errorf("List() got %d prompts, want at least 5", len(prompts))
+ // Should have all prompts
+ if len(prompts) != 7 {
+ t.Errorf("List() got %d prompts, want 7", len(prompts))
}
// No cursor for full list
@@ -98,10 +109,10 @@ func TestJSONLStore_List(t *testing.T) {
if err != nil {
t.Fatalf("List() error = %v", err)
}
- if len(prompts2) == 0 {
- t.Error("List() second page empty")
+ if len(prompts2) != 3 {
+ t.Errorf("List() got %d prompts on page 2, want 3", len(prompts2))
}
- if cursor2 == "" && len(prompts) > 6 {
+ if cursor2 == "" {
t.Error("List() expected cursor2, got empty")
}
}
@@ -236,14 +247,50 @@ func TestJSONLStore_SearchByTags(t *testing.T) {
t.Fatalf("NewJSONLStore() error = %v", err)
}
+ // Create test prompts with tags
+ prompt1 := &Prompt{
+ Name: "test1",
+ Title: "Test 1",
+ Tags: []string{"development", "review"},
+ Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test 1"}}},
+ Created: time.Now(),
+ Updated: time.Now(),
+ }
+ prompt2 := &Prompt{
+ Name: "test2",
+ Title: "Test 2",
+ Tags: []string{"development"},
+ Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test 2"}}},
+ Created: time.Now(),
+ Updated: time.Now(),
+ }
+ prompt3 := &Prompt{
+ Name: "test3",
+ Title: "Test 3",
+ Tags: []string{"testing"},
+ Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Test 3"}}},
+ Created: time.Now(),
+ Updated: time.Now(),
+ }
+
+ if err := store.Create(prompt1); err != nil {
+ t.Fatalf("Create() error = %v", err)
+ }
+ if err := store.Create(prompt2); err != nil {
+ t.Fatalf("Create() error = %v", err)
+ }
+ if err := store.Create(prompt3); err != nil {
+ t.Fatalf("Create() error = %v", err)
+ }
+
// Search for development tag
results, err := store.SearchByTags([]string{"development"})
if err != nil {
t.Fatalf("SearchByTags() error = %v", err)
}
- if len(results) < 3 {
- t.Errorf("SearchByTags() got %d results, want at least 3", len(results))
+ if len(results) != 2 {
+ t.Errorf("SearchByTags() got %d results, want 2", len(results))
}
// Search for multiple tags
@@ -252,60 +299,10 @@ func TestJSONLStore_SearchByTags(t *testing.T) {
t.Fatalf("SearchByTags() error = %v", err)
}
- // Should find code_review prompt
- found := false
- for _, p := range results {
- if p.Name == "code_review" {
- found = true
- break
- }
- }
- if !found {
- t.Error("SearchByTags() should find code_review prompt")
- }
-}
-
-func TestBuiltinPrompts(t *testing.T) {
- prompts := GetBuiltinPrompts()
-
- if len(prompts) < 5 {
- t.Errorf("GetBuiltinPrompts() got %d prompts, want at least 5", len(prompts))
- }
-
- // Verify each prompt has required fields
- for _, p := range prompts {
- if p.Name == "" {
- t.Error("Prompt missing name")
- }
- if p.Title == "" {
- t.Errorf("Prompt %s missing title", p.Name)
- }
- if len(p.Messages) == 0 {
- t.Errorf("Prompt %s has no messages", p.Name)
- }
+ if len(results) != 1 {
+ t.Errorf("SearchByTags() got %d results, want 1", len(results))
}
-}
-
-func TestJSONLStore_DefaultFileCreation(t *testing.T) {
- tmpDir := t.TempDir()
- _, err := NewJSONLStore(tmpDir)
- if err != nil {
- t.Fatalf("NewJSONLStore() error = %v", err)
- }
-
- // Verify default.jsonl was created
- defaultPath := filepath.Join(tmpDir, "default.jsonl")
- if _, err := os.Stat(defaultPath); os.IsNotExist(err) {
- t.Error("default.jsonl was not created")
- }
-
- // Verify it contains prompts
- data, err := os.ReadFile(defaultPath)
- if err != nil {
- t.Fatalf("ReadFile() error = %v", err)
- }
-
- if len(data) == 0 {
- t.Error("default.jsonl is empty")
+ if results[0].Name != "test1" {
+ t.Errorf("SearchByTags() got prompt %s, want test1", results[0].Name)
}
}