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 /internal/promptstore/store.go | |
| 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>
Diffstat (limited to 'internal/promptstore/store.go')
| -rw-r--r-- | internal/promptstore/store.go | 83 |
1 files changed, 75 insertions, 8 deletions
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. |
