summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-11 22:14:42 +0200
committerPaul Buetow <paul@buetow.org>2026-02-11 22:14:42 +0200
commitd3810ca268f8db2867ae838d0655fb7a56e67252 (patch)
tree23c18a31f35f1d94249e50d3e66a66e4f9ec7853
parenta82d0b061a02fd395de293353386d0b16cbe6b18 (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--.gitignore1
-rw-r--r--README.md5
-rw-r--r--docs/mcp-prompts.md181
-rw-r--r--internal/promptstore/default_prompts.go149
-rw-r--r--internal/promptstore/store.go83
-rw-r--r--internal/promptstore/store_test.go257
6 files changed, 618 insertions, 58 deletions
diff --git a/.gitignore b/.gitignore
index 43ae798..e996cb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
/hexai
/hexai-lsp
+/hexai-mcp-server
/hexai-tmux-action
/hexai-tmux-edit
/bin/
diff --git a/README.md b/README.md
index 18f6c6f..71d3b39 100644
--- a/README.md
+++ b/README.md
@@ -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
+}