summaryrefslogtreecommitdiff
path: root/internal/promptstore/store.go
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 /internal/promptstore/store.go
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>
Diffstat (limited to 'internal/promptstore/store.go')
-rw-r--r--internal/promptstore/store.go83
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.