diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-11 22:14:42 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-11 22:14:42 +0200 |
| commit | d3810ca268f8db2867ae838d0655fb7a56e67252 (patch) | |
| tree | 23c18a31f35f1d94249e50d3e66a66e4f9ec7853 | |
| parent | a82d0b061a02fd395de293353386d0b16cbe6b18 (diff) | |
refactor: compile built-in prompts into binary instead of external files
This change moves built-in meta-prompts (save_prompt, update_prompt) from
external JSONL files into compiled Go code, making them always available
and version-controlled with the binary.
Changes:
- Add default_prompts.go with built-in meta-prompt definitions
- Update store to load built-ins from code, not files
- Add protection: built-ins cannot be updated/deleted
- Handle name conflicts: built-ins take precedence with warnings
- Update docs to reflect new architecture (no default.jsonl needed)
- Add comprehensive tests for built-in protection
- Add hexai-mcp-server binary to .gitignore
Benefits:
- Built-ins always in sync with binary version
- No setup required (no default.jsonl to manage)
- Clear separation between built-in and user prompts
- Protection prevents accidental modification of meta-prompts
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | docs/mcp-prompts.md | 181 | ||||
| -rw-r--r-- | internal/promptstore/default_prompts.go | 149 | ||||
| -rw-r--r-- | internal/promptstore/store.go | 83 | ||||
| -rw-r--r-- | internal/promptstore/store_test.go | 257 |
6 files changed, 618 insertions, 58 deletions
@@ -1,5 +1,6 @@ /hexai /hexai-lsp +/hexai-mcp-server /hexai-tmux-action /hexai-tmux-edit /bin/ @@ -18,7 +18,7 @@ It has got improved capabilities for Go code understanding (for example, create - Automatic backups on every change (keeps last 10) - Compatible with Claude Code CLI, Cursor, and other MCP clients - File-based storage with JSONL format (git-friendly) - - Built-in prompts for code review, testing, documentation, etc. + - Built-in meta-prompts for interactive prompt creation and management * TUI AI code-action runner (`hexai-tmux-action`) with Bubble Tea - Includes a "Custom prompt" action (hotkey `p`) that opens your editor (`$HEXAI_EDITOR` or `$EDITOR`) on a temporary Markdown file. * Tmux popup editor (`hexai-tmux-edit`) for composing longer AI agent prompts @@ -56,7 +56,6 @@ hexai follows the XDG Base Directory Specification: - `hexai-tmux-edit.log` - Tmux edit debug logs - `hexai-mcp-server.log` - MCP server debug logs - **Data:** `~/.local/hexai/data/` (or `$XDG_DATA_HOME/`) - - `prompts/default.jsonl` - Built-in prompts for MCP server - - `prompts/user.jsonl` - User-created custom prompts + - `prompts/user.jsonl` - User-created custom prompts (built-in prompts are compiled into the binary) - **Temporary Files:** `/tmp/` (OS temp directory) - `hexai-*.md` - Temporary editor workspaces (auto-deleted) diff --git a/docs/mcp-prompts.md b/docs/mcp-prompts.md index 2297281..777af5b 100644 --- a/docs/mcp-prompts.md +++ b/docs/mcp-prompts.md @@ -81,78 +81,171 @@ Messages define the conversation flow: ## Built-in Prompts -hexai-mcp-server includes these built-in prompts: +hexai-mcp-server includes built-in **meta-prompts** that help you create and manage prompts interactively. These prompts leverage Claude's access to your conversation context to extract content and create templates. -### code_review -Analyzes code quality, style, and potential issues. +### save_prompt +Interactively create a new prompt template from your current conversation. + +**How it works**: +1) You provide a name and title for the new prompt +2) Claude analyzes your conversation to understand what should be templated +3) Claude asks clarifying questions about arguments, description, and tags +4) Claude shows a complete preview of the prompt structure +5) After you approve, Claude saves it using the MCP prompts/create method **Arguments**: -- `code` (required): The code to review +- `prompt_name` (required): Unique identifier (lowercase, underscores allowed) +- `prompt_title` (required): Human-readable display name -**Tags**: development, review, quality +**Usage Example**: +``` +You: (Have a conversation about code review) + "Can you help me review Go code for best practices?" +Claude: (Provides code review suggestions) -### explain_code -Provides detailed explanation of what code does. +You: /save_prompt prompt_name="go_best_practices" prompt_title="Go Best Practices Review" -**Arguments**: -- `code` (required): The code to explain +Claude: I'll help you create this prompt. Looking at our conversation, I see you want + a code review focused on Go best practices. -**Tags**: development, documentation, learning + Clarifying Questions: -### generate_tests -Generates unit tests for a function or class. + 1) Template Arguments + Should the 'code' argument be required? + a) Yes, required (can't review without code) + b) No, optional -**Arguments**: -- `code` (required): The code to test -- `language` (optional): Programming language + 2) Description + What description would best explain this prompt's purpose? + a) "Reviews Go code for adherence to best practices and idiomatic patterns" + b) "Analyzes Go code quality focusing on style, performance, and Go idioms" + c) Something else? -**Tags**: development, testing, tdd + 3) Tags + Which tags would help categorize it? + a) ["go", "review", "best-practices"] + b) ["development", "go", "code-quality"] + c) Other suggestions? -### document_function -Generates documentation comments and docstrings. + ... -**Arguments**: -- `code` (required): The code to document + Here's the preview: + ```json + { + "name": "go_best_practices", + "title": "Go Best Practices Review", + ... + } + ``` -**Tags**: development, documentation + Should I save this prompt? -### simplify_code -Simplifies complex code while preserving behavior. +You: Yes, looks good! -**Arguments**: -- `code` (required): The code to simplify +Claude: (Saves the prompt using MCP prompts/create) +``` -**Tags**: development, refactoring, quality +**Tags**: meta, prompt-management, interactive -### fix_bugs -Analyzes code for bugs and suggests fixes. +### update_prompt +Interactively modify an existing prompt. + +**How it works**: +1) You provide the name of the prompt to update +2) Claude fetches and shows the current version +3) Claude asks what changes you want to make +4) If you reference conversation content, Claude helps extract and template it +5) Claude shows a preview with changes highlighted +6) After you approve, Claude updates it using the MCP prompts/update method **Arguments**: -- `code` (required): The code to analyze -- `error` (optional): Error message or symptoms +- `prompt_name` (required): Name of the existing prompt to update -**Tags**: development, debugging, bug-fix +**Usage Example**: +``` +You: /update_prompt prompt_name="go_best_practices" -### refactor_extract -Extracts code into a separate, reusable function. +Claude: Let me fetch the current version of 'go_best_practices'... -**Arguments**: -- `code` (required): The code to extract -- `function_name` (optional): Desired function name + Current prompt: + - Name: go_best_practices + - Title: Go Best Practices Review + - Description: Reviews Go code for idiomatic patterns + - Arguments: code (required) + + What changes would you like to make? + +You: Add an optional 'focus_area' argument that lets me specify what aspect to focus on -**Tags**: development, refactoring +Claude: Great! I'll add that argument. Here's the updated preview with changes: + + ```json + { + "name": "go_best_practices", + "arguments": [ + {"name": "code", "required": true, ...}, + {"name": "focus_area", "required": false, ...} // ← NEW + ], + "messages": [ + { + "text": "Review this Go code for best practices:\n\n{{code}}\n\nFocus on: {{focus_area}}" // ← UPDATED + } + ] + } + ``` + + Should I save these changes? + +You: Yes! + +Claude: (Updates the prompt using MCP prompts/update) +``` + +**Tags**: meta, prompt-management, interactive + +**Note**: Built-in prompts (including these meta-prompts) cannot be modified or deleted. If you need to customize a built-in, create a new prompt with a different name. ## Creating Custom Prompts ### Storage Files -Prompts are stored in two files: -- `default.jsonl`: Built-in prompts (automatically created) -- `user.jsonl`: Your custom prompts +Prompts are stored as follows: +- **Built-in prompts**: Compiled into the `hexai-mcp-server` binary (no file needed) +- **User prompts**: Stored in `user.jsonl` at `~/.local/share/hexai/prompts/` (or your configured directory) + +This means built-in prompts are always available and up-to-date with the binary version. + +### Method 1: Interactive Meta-Prompts (Recommended) + +Use the built-in `save_prompt` meta-prompt to create prompts interactively. This is the easiest method because: +- Claude extracts content from your current conversation +- You get guided questions about templating +- You see a preview before saving +- No manual JSON editing required + +**Example workflow**: +``` +1) Have a conversation with Claude about your task + You: "Help me write better commit messages" + Claude: (Provides guidance on commit message best practices) + +2) Invoke the meta-prompt + You: /save_prompt prompt_name="better_commits" prompt_title="Better Commit Messages" + +3) Answer Claude's clarifying questions + Claude: "Should I template the 'diff' as an argument?" + You: "Yes, make it required" + +4) Review and approve + Claude: (Shows JSON preview) + You: "Looks good!" + +5) Prompt is saved to user.jsonl +``` -Both files are in: `~/.local/hexai/data/prompts/` (or your configured directory) +To modify an existing prompt, use `/update_prompt prompt_name="better_commits"`. -### Method 1: Manual Editing +### Method 2: Manual Editing Edit `user.jsonl` directly: @@ -172,7 +265,7 @@ Add a new prompt (one line, formatted for readability here): cat user.jsonl | jq . ``` -### Method 2: Python Script +### Method 3: Python Script Create prompts programmatically: @@ -215,7 +308,7 @@ with open("~/.local/hexai/data/prompts/user.jsonl", "a") as f: f.write(json.dumps(prompt) + "\n") ``` -### Method 3: Go Code +### Method 4: Go Code Use hexai's promptstore package: diff --git a/internal/promptstore/default_prompts.go b/internal/promptstore/default_prompts.go new file mode 100644 index 0000000..ea8c793 --- /dev/null +++ b/internal/promptstore/default_prompts.go @@ -0,0 +1,149 @@ +// Summary: Built-in meta-prompts for prompt management. +package promptstore + +import ( + "time" +) + +// DefaultPrompts returns the built-in meta-prompts for prompt management. +// These prompts help users create and update prompts interactively using Claude's +// access to conversation context. +func DefaultPrompts() []Prompt { + now := time.Now() + + return []Prompt{ + { + Name: "save_prompt", + Title: "Save Current Conversation as Prompt", + Description: "Interactively create a new prompt template from the current conversation. Claude will analyze the conversation, ask clarifying questions about templating, show a preview, and wait for approval before saving.", + Arguments: []PromptArgument{ + { + Name: "prompt_name", + Description: "Unique identifier for the new prompt (lowercase, underscores allowed)", + Required: true, + }, + { + Name: "prompt_title", + Description: "Human-readable display name for the new prompt", + Required: true, + }, + }, + Messages: []PromptMessage{ + { + Role: "user", + Content: MessageContent{ + Type: "text", + Text: `I want to create a new prompt template named '{{prompt_name}}' with title '{{prompt_title}}'. + +Please help me by: +1) Analyzing our current conversation to understand what should be templated +2) Asking me clarifying questions about: + - What parts should be template arguments vs fixed text + - What description would best explain this prompt's purpose + - What tags would help categorize it + - Whether multi-turn messages are needed +3) Showing me a complete preview of the prompt structure in a code block +4) Only after I approve, use the MCP prompts/create method to save it + +IMPORTANT FORMATTING RULES for clarifying questions: +- Use numbered questions: 1), 2), 3) +- ANY CHOICE MUST BE NUMBERED using combined format: 1a), 1b), 1c), 2a), 2b), etc. +- NEVER use standalone letters like "a)" - always combine with question number +- NEVER use dashes (-) or bullets (•) for options +- Every option must be numbered for easy selection by the user + +Examples: + 1) Question Category + Which do you prefer? + 1a) First option + 1b) Second option + 1c) Third option + + 2) Arguments + Should this accept parameters? + 2a) No arguments - fixed behavior + 2b) Optional file_pattern argument + 2c) Multiple optional arguments + + 3) Sub-items + Consider these aspects: + 3a) First aspect to consider + 3b) Second aspect to consider + 3c) Third aspect to consider + + 4) Multiple sub-questions + 4a) Sub-question one? + Answer options here + 4b) Sub-question two? + Answer options here + +Start by examining our conversation and asking your clarifying questions using this format.`, + }, + }, + }, + Tags: []string{"meta", "prompt-management", "interactive"}, + Created: now, + Updated: now, + }, + { + Name: "update_prompt", + Title: "Update Existing Prompt", + Description: "Interactively modify an existing prompt. Claude will fetch the current version, ask what changes you want, show a preview with changes highlighted, and wait for approval before updating.", + Arguments: []PromptArgument{ + { + Name: "prompt_name", + Description: "Name of the existing prompt to update", + Required: true, + }, + }, + Messages: []PromptMessage{ + { + Role: "user", + Content: MessageContent{ + Type: "text", + Text: `I want to update the existing prompt '{{prompt_name}}'. + +Please help me by: +1) First, retrieve the current prompt using prompts/get to show me what exists +2) Ask me what changes I want to make (description, arguments, messages, tags) +3) If I reference content from our current conversation, help extract and template it +4) Show me a complete preview of the updated prompt with changes highlighted +5) Only after I approve, use the MCP prompts/update method to save the changes + +IMPORTANT FORMATTING RULES for clarifying questions: +- Use numbered questions: 1), 2), 3) +- ANY CHOICE MUST BE NUMBERED using combined format: 1a), 1b), 1c), 2a), 2b), etc. +- NEVER use standalone letters like "a)" - always combine with question number +- NEVER use dashes (-) or bullets (•) for options +- Every option must be numbered for easy selection by the user + +Examples: + 1) Question Category + Which do you prefer? + 1a) First option + 1b) Second option + 1c) Third option + + 2) Changes to Make + What aspects should be updated? + 2a) Description only + 2b) Arguments only + 2c) Both description and arguments + 2d) Complete rewrite + + 3) Multiple aspects + Consider: + 3a) First aspect to evaluate + 3b) Second aspect to evaluate + 3c) Third aspect to evaluate + +Start by fetching and showing me the current prompt, then ask clarifying questions using this format.`, + }, + }, + }, + Tags: []string{"meta", "prompt-management", "interactive"}, + Created: now, + Updated: now, + }, + } +} diff --git a/internal/promptstore/store.go b/internal/promptstore/store.go index e789dda..b4b9586 100644 --- a/internal/promptstore/store.go +++ b/internal/promptstore/store.go @@ -37,7 +37,7 @@ type PromptStore interface { } // JSONLStore is a file-based prompt store using JSONL format. -// Stores prompts in multiple JSONL files (default.jsonl for built-ins, user.jsonl for custom). +// Built-in prompts are loaded from code, user prompts are stored in user.jsonl. // Automatically creates backups before any write operation. type JSONLStore struct { dataDir string @@ -76,7 +76,7 @@ func NewJSONLStore(dataDir string) (PromptStore, error) { } // List returns prompts with pagination. -// cursor format: "<file>:<offset>" where file is "default" or "user", offset is line number. +// Returns both built-in prompts (from code) and user prompts (from user.jsonl). func (s *JSONLStore) List(cursor string, limit int) ([]Prompt, string, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -150,12 +150,21 @@ func (s *JSONLStore) Create(prompt *Prompt) error { return fmt.Errorf("backup failed: %w", err) } - // Check if prompt already exists (use internal method to avoid deadlock) - allPrompts, err := s.loadAllPrompts() + // Check if prompt already exists in built-ins + isBuiltIn, err := s.isBuiltInPrompt(prompt.Name) if err != nil { - return fmt.Errorf("load prompts: %w", err) + return fmt.Errorf("check built-in prompts: %w", err) } - for _, p := range allPrompts { + if isBuiltIn { + return fmt.Errorf("prompt already exists: %s (choose a different name)", prompt.Name) + } + + // Check if prompt already exists in user prompts + userPrompts, err := s.loadPromptsFromFile("user.jsonl") + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("load user prompts: %w", err) + } + for _, p := range userPrompts { if p.Name == prompt.Name { return fmt.Errorf("prompt already exists: %s", prompt.Name) } @@ -184,10 +193,20 @@ func (s *JSONLStore) Create(prompt *Prompt) error { // Update modifies an existing prompt in user.jsonl. // Note: This rewrites the entire user.jsonl file. +// Cannot update built-in prompts (returns error). func (s *JSONLStore) Update(prompt *Prompt) error { s.mu.Lock() defer s.mu.Unlock() + // Check if this is a built-in prompt (cannot be updated) + isBuiltIn, err := s.isBuiltInPrompt(prompt.Name) + if err != nil { + return fmt.Errorf("check built-in prompts: %w", err) + } + if isBuiltIn { + return fmt.Errorf("cannot update built-in prompt: %s (create a new prompt with a different name instead)", prompt.Name) + } + // Backup before write if err := s.backupUserPrompts(); err != nil { return fmt.Errorf("backup failed: %w", err) @@ -218,10 +237,20 @@ func (s *JSONLStore) Update(prompt *Prompt) error { } // Delete removes a prompt from user.jsonl. +// Cannot delete built-in prompts (returns error). func (s *JSONLStore) Delete(name string) error { s.mu.Lock() defer s.mu.Unlock() + // Check if this is a built-in prompt (cannot be deleted) + isBuiltIn, err := s.isBuiltInPrompt(name) + if err != nil { + return fmt.Errorf("check built-in prompts: %w", err) + } + if isBuiltIn { + return fmt.Errorf("cannot delete built-in prompt: %s", name) + } + // Backup before write if err := s.backupUserPrompts(); err != nil { return fmt.Errorf("backup failed: %w", err) @@ -291,14 +320,52 @@ func (s *JSONLStore) hasAllTags(promptTags, searchTags []string) bool { return true } -// loadAllPrompts loads prompts from user.jsonl. +// loadAllPrompts loads prompts from both built-in code and user.jsonl. +// Built-in prompts (from code) take precedence in case of name conflicts. +// Logs a warning to stderr if a user prompt conflicts with a built-in. func (s *JSONLStore) loadAllPrompts() ([]Prompt, error) { + // Load built-in prompts directly from code (no file needed) + builtInPrompts := DefaultPrompts() + + // Load user prompts from user.jsonl userPrompts, err := s.loadPromptsFromFile("user.jsonl") if err != nil && !os.IsNotExist(err) { return nil, err } - return userPrompts, nil + // Create a map of built-in prompt names for conflict detection + builtInNames := make(map[string]bool) + for _, p := range builtInPrompts { + builtInNames[p.Name] = true + } + + // Combine prompts, skipping user prompts that conflict with built-ins + allPrompts := make([]Prompt, 0, len(builtInPrompts)+len(userPrompts)) + allPrompts = append(allPrompts, builtInPrompts...) + + for _, p := range userPrompts { + if builtInNames[p.Name] { + fmt.Fprintf(os.Stderr, "warning: skipping user prompt '%s' - conflicts with built-in\n", p.Name) + continue + } + allPrompts = append(allPrompts, p) + } + + return allPrompts, nil +} + +// isBuiltInPrompt checks if a prompt with the given name exists in the built-in prompts. +// Returns true if the prompt is a built-in (read-only) prompt. +func (s *JSONLStore) isBuiltInPrompt(name string) (bool, error) { + builtIns := DefaultPrompts() + + for _, p := range builtIns { + if p.Name == name { + return true, nil + } + } + + return false, nil } // loadPromptsFromFile reads prompts from a JSONL file. diff --git a/internal/promptstore/store_test.go b/internal/promptstore/store_test.go index 167dcf6..6e74b17 100644 --- a/internal/promptstore/store_test.go +++ b/internal/promptstore/store_test.go @@ -3,6 +3,8 @@ package promptstore import ( "fmt" + "os" + "path/filepath" "testing" "time" ) @@ -82,9 +84,9 @@ func TestJSONLStore_List(t *testing.T) { t.Fatalf("List() error = %v", err) } - // Should have all prompts - if len(prompts) != 7 { - t.Errorf("List() got %d prompts, want 7", len(prompts)) + // Should have all prompts (7 user + 2 built-ins) + if len(prompts) != 9 { + t.Errorf("List() got %d prompts, want 9 (7 user + 2 built-ins)", len(prompts)) } // No cursor for full list @@ -306,3 +308,252 @@ func TestJSONLStore_SearchByTags(t *testing.T) { t.Errorf("SearchByTags() got prompt %s, want test1", results[0].Name) } } + +func TestJSONLStore_LoadAllPrompts_CodeAndFile(t *testing.T) { + t.Run("loads from both code (built-ins) and user.jsonl", func(t *testing.T) { + tmpDir := t.TempDir() + store, err := NewJSONLStore(tmpDir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + // Create a user prompt + userPrompt := &Prompt{ + Name: "user_test", + Title: "User Test", + Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "User test"}}}, + Created: time.Now(), + Updated: time.Now(), + } + if err := store.Create(userPrompt); err != nil { + t.Fatalf("Create() error = %v", err) + } + + // List all prompts (should include both built-ins from code and user prompt) + prompts, _, err := store.List("", 100) + if err != nil { + t.Fatalf("List() error = %v", err) + } + + // Check we have at least the built-ins (save_prompt, update_prompt) + user prompt + if len(prompts) < 3 { + t.Errorf("List() got %d prompts, want at least 3 (2 built-ins + 1 user)", len(prompts)) + } + + // Verify built-in prompts are present + hasBuiltIn := false + hasUser := false + for _, p := range prompts { + if p.Name == "save_prompt" || p.Name == "update_prompt" { + hasBuiltIn = true + } + if p.Name == "user_test" { + hasUser = true + } + } + + if !hasBuiltIn { + t.Error("List() missing built-in prompts (save_prompt or update_prompt)") + } + if !hasUser { + t.Error("List() missing user prompt (user_test)") + } + }) + + t.Run("built-ins take precedence over user prompts with same name", func(t *testing.T) { + tmpDir := t.TempDir() + store, err := NewJSONLStore(tmpDir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + // Get the built-in save_prompt (from code) + builtIn, err := store.Get("save_prompt") + if err != nil { + t.Fatalf("Get(save_prompt) error = %v", err) + } + originalTitle := builtIn.Title + + // Manually add a conflicting prompt to user.jsonl (bypass protection) + // This simulates a conflict scenario + jStore := store.(*JSONLStore) + conflictPrompt := &Prompt{ + Name: "save_prompt", + Title: "Conflicting Title", + Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Conflict"}}}, + Created: time.Now(), + Updated: time.Now(), + } + // Write directly to user.jsonl without using Create (which has protection) + userPrompts := []Prompt{*conflictPrompt} + if err := jStore.writePromptsToFile("user.jsonl", userPrompts); err != nil { + t.Fatalf("writePromptsToFile() error = %v", err) + } + + // Get save_prompt again - should return built-in from code, not user version + result, err := store.Get("save_prompt") + if err != nil { + t.Fatalf("Get(save_prompt) after conflict error = %v", err) + } + + if result.Title != originalTitle { + t.Errorf("Get(save_prompt) title = %v, want %v (built-in should take precedence)", result.Title, originalTitle) + } + }) +} + +func TestJSONLStore_BuiltInProtection_Update(t *testing.T) { + tmpDir := t.TempDir() + store, err := NewJSONLStore(tmpDir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + // Get the built-in save_prompt + builtIn, err := store.Get("save_prompt") + if err != nil { + t.Fatalf("Get(save_prompt) error = %v", err) + } + + // Try to update it + builtIn.Title = "Modified Title" + err = store.Update(builtIn) + + // Should fail with clear error message + if err == nil { + t.Fatal("Update() on built-in prompt should fail") + } + + expectedMsg := "cannot update built-in prompt: save_prompt" + if !contains(err.Error(), expectedMsg) { + t.Errorf("Update() error = %v, want error containing %q", err, expectedMsg) + } +} + +func TestJSONLStore_BuiltInProtection_Delete(t *testing.T) { + tmpDir := t.TempDir() + store, err := NewJSONLStore(tmpDir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + // Verify save_prompt exists + _, err = store.Get("save_prompt") + if err != nil { + t.Fatalf("Get(save_prompt) error = %v (built-in should exist)", err) + } + + // Try to delete it + err = store.Delete("save_prompt") + + // Should fail with clear error message + if err == nil { + t.Fatal("Delete() on built-in prompt should fail") + } + + expectedMsg := "cannot delete built-in prompt: save_prompt" + if !contains(err.Error(), expectedMsg) { + t.Errorf("Delete() error = %v, want error containing %q", err, expectedMsg) + } + + // Verify it still exists + _, err = store.Get("save_prompt") + if err != nil { + t.Error("Get(save_prompt) after failed delete should still work") + } +} + +func TestJSONLStore_Create_NameConflictWithBuiltIn(t *testing.T) { + tmpDir := t.TempDir() + store, err := NewJSONLStore(tmpDir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + // Try to create a prompt with the same name as a built-in + conflictPrompt := &Prompt{ + Name: "save_prompt", + Title: "My Custom Save", + Messages: []PromptMessage{{Role: "user", Content: MessageContent{Type: "text", Text: "Custom"}}}, + Created: time.Now(), + Updated: time.Now(), + } + + err = store.Create(conflictPrompt) + + // Should fail with clear error message + if err == nil { + t.Fatal("Create() with built-in name should fail") + } + + expectedMsg := "prompt already exists: save_prompt" + if !contains(err.Error(), expectedMsg) { + t.Errorf("Create() error = %v, want error containing %q", err, expectedMsg) + } +} + +func TestJSONLStore_BuiltInPromptsLoadedFromCode(t *testing.T) { + tmpDir := t.TempDir() + + // Create store (built-ins loaded from code, not from file) + store, err := NewJSONLStore(tmpDir) + if err != nil { + t.Fatalf("NewJSONLStore() error = %v", err) + } + + // Verify save_prompt exists (loaded from code) + savePrompt, err := store.Get("save_prompt") + if err != nil { + t.Fatalf("Get(save_prompt) error = %v (should be loaded from code)", err) + } + if savePrompt.Name != "save_prompt" { + t.Errorf("Get(save_prompt) name = %v, want save_prompt", savePrompt.Name) + } + + // Verify update_prompt exists (loaded from code) + updatePrompt, err := store.Get("update_prompt") + if err != nil { + t.Fatalf("Get(update_prompt) error = %v (should be loaded from code)", err) + } + if updatePrompt.Name != "update_prompt" { + t.Errorf("Get(update_prompt) name = %v, want update_prompt", updatePrompt.Name) + } + + // Verify both have correct tags + if !containsTag(savePrompt.Tags, "meta") { + t.Error("save_prompt should have 'meta' tag") + } + if !containsTag(updatePrompt.Tags, "meta") { + t.Error("update_prompt should have 'meta' tag") + } + + // Verify no default.jsonl file was created + defaultPath := filepath.Join(tmpDir, "default.jsonl") + if _, err := os.Stat(defaultPath); !os.IsNotExist(err) { + t.Error("default.jsonl should not be created (built-ins loaded from code)") + } +} + +// Helper function to check if a string contains a substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsSubstring(s, substr)) +} + +func containsSubstring(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +// Helper function to check if a slice contains a tag +func containsTag(tags []string, tag string) bool { + for _, t := range tags { + if t == tag { + return true + } + } + return false +} |
